I have one component with a form that creates posts and then I have another component that displays these posts in a feed. How do I get the feed to re-render when a new post is created? What's the best way to do that?
Re-render the parent component without changing props?
Update props?
Some other way?
Create.js
import React, { useState } from "react";
export default function Create () {
const [form, setForm] = useState({
post: "",
});
// These methods will update the state properties.
function updateForm(value) {
return setForm((prev) => {
return { ...prev, ...value };
});
}
async function onSubmit(e) {
e.preventDefault();
// When a post request is sent to the create url, we'll add a new post to the database.
const newPost = { ...form };
await fetch("http://localhost:3000/post/add", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(newPost),
})
.catch(error => {
window.alert(error);
return;
});
setForm({ post: "" });
}
return (
<div>
<form onSubmit={onSubmit}>
<div className="form-group">
<label htmlFor="post">Post</label>
<input
type="text"
className="form-control"
id="post"
value={form.post}
onChange={(e) => updateForm( { post: e.target.value })}
/>
</div>
<div className="form-group">
<input
type="submit"
value="Create post"
className="btn btn-primary"
/>
</div>
</form>
</div>
);
}
Feed.js
import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom";
const Post = (props) => (
<div>
{props.post.post}
<Link className="btn btn-link" to={`/edit/${props.post._id}`}>Edit</Link>
<button className="btn btn-link" onClick={() => { props.deletePost(props.post._id);
}}
>
Delete
</button>
</div>
);
export default function PostList() {
const [posts, setPosts] = useState([]);
// This method fetches the posts from the database.
useEffect(() => {
async function getPosts() {
const response = await fetch(`http://localhost:3000/post/`);
if (!response.ok) {
const message = `An error occured: ${response.statusText}`;
window.alert(message);
return;
}
const posts = await response.json();
setPosts(posts);
}
getPosts();
return;
}, [posts.length]);
// This method will delete a post
async function deletePost(id) {
await fetch(`http://localhost:3000/${id}`, {
method: "DELETE"
});
const newPosts = posts.filter((el) => el._id !== id);
setPosts(newPosts);
}
// This method will map out the posts on the table
function postList() {
return posts.map((post) => {
return (
<Post
post={post}
deletePost={() => deletePost(post._id)}
key={post._id}
/>
);
});
}
// This following section will display the the posts.
return (
<div>
<h3>Post List</h3>
<div>{postList()}</div>
</div>
);
}
Home.js
import React from "react";
import Feed from "./feed";
import Create from "./create";
const Home = () => {
return (
<div className="app">
<div>
<Create />
<Feed />
</div>
</div>
);
};
export default Home;
There are numerous ways to do so. The one you should start with is the most straightforward: pass your current posts and setPosts into both component from a parent component:
const Parent = () => {
const [posts, setPosts] = useState([]);
return <>
<Create posts={posts} setPosts={setPosts} />
<PostList posts={posts} setPosts={setPosts} />
</>
}
There are better ways to do so, but they are more involved. I suggest you to start with this one to understand how React handles data better.
Related
I am trying to get the id from a doc when I click a delete button. so the user can delete the post.
I got far enough to show the button if the user is the uploader.
but, I don't know how to get the id from the clicked document.
I tried this:
{auth.currentUser.uid === uploaderId ? (
<div
onClick={(e) => {
if (auth.currentUser.uid === uploaderId) {
e.stopPropagation();
deleteDoc(doc(db, "posts", id));
}
}}
>
<div>
<button>🗑️</button>
</div>
</div>
) : (
<div>
</div>
)}
but that doesn't work and gives me the error:
Uncaught TypeError: Cannot read properties of undefined (reading 'indexOf')
so to summarize how do I get the id from the document
full code:
import React, { useEffect, useRef, useState } from 'react';
import './App.css';
import { initializeApp } from "firebase/app";
import { getFirestore, collection, orderBy,
query, Timestamp, addDoc, limitToLast,
deleteDoc, doc, } from "firebase/firestore";
import { confirmPasswordReset, getAuth, GoogleAuthProvider, signInWithPopup, } from "firebase/auth";
import { useAuthState } from "react-firebase-hooks/auth";
import { useCollectionData } from "react-firebase-hooks/firestore";
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const auth = getAuth(app);
const db = getFirestore(app);
function App() {
const [user] = useAuthState(auth);
return (
<div className="App">
<Header/>
{/* <Picker/> */}
<section>
<SignOut/>
{/* <ShowProfile/> */}
<Upload/>
{user ? <Feed /> : <SignIn />}
</section>
</div>
);
}
function Header() {
return (
<h1 className='headerTxt'>SHED</h1>
)
}
// function Picker() {
// }
function SignIn() {
const signInWithGoogle = () =>{
const provider = new GoogleAuthProvider();
signInWithPopup(auth, provider)
}
return (
<button className='SignIn' onClick={signInWithGoogle}>sign in with google</button>
)
}
function SignOut() {
return auth.currentUser && (
<button onClick={() => auth.signOut()}>Sign out</button>
)
}
function Feed() {
const postsRef = collection(db, 'posts');
const qwery = query(postsRef,orderBy('createdAt'), limitToLast(25), );
const [posts] = useCollectionData(qwery, {idField: "id"});
return (
<>
<main>
{posts && posts.map(msg => <Post key={msg.id} post={msg} />)}
</main>
</>
)
}
function Post(props) {
const {text, photoURL, displayName, uid, uploaderId, id} = props.post;
return (
<div className={"post"}>
<div className='msg'>
<div className='top'>
<img src={photoURL} alt="" />
<sub>{displayName}</sub>
</div>
<hr />
<p>{text}</p>
{auth.currentUser.uid === uploaderId ? (
<div
onClick={(e) => {
if (auth.currentUser.uid === uploaderId) {
e.stopPropagation();
deleteDoc(doc(db, "posts", id));
}
}}
>
<div>
<button>🗑️</button>
</div>
</div>
) : (
<div>
</div>
)}
{/* <button>❤️</button> */}
</div>
</div>
)
}
// function ShowProfile() {
// return(
// <div>
// <button onClick={queryMine}>my posts</button>
// </div>
// )
// }
function Upload() {
const postsRef = collection(db, 'posts');
const [formValue, setFormValue] = useState('');
const sendpost = async(e) =>{
e.preventDefault();
const {uid, photoURL, displayName} = auth.currentUser;
if (formValue !== "" && formValue !== " ") {
await addDoc(postsRef, {
text: formValue,
createdAt: Timestamp.now(),
displayName,
uploaderId: uid,
photoURL
})
setFormValue('')
}
}
return auth.currentUser &&(
<form onSubmit={sendpost}>
<textarea name="" id="" cols="30" rows="10"
value={formValue} onChange={(e) => setFormValue(e.target.value)}
></textarea>
<button type='submit'>➜</button>
</form>
)
}
export default App;```
Get the data (doc id and doc data) using querySnapshot and merge them into one object. Save an array of objects to a state and pass what you need as properties. For example:
...
const querySnapshot = await getDocs(collection(db, `users/${email}/todos`))
const todos = []
querySnapshot.forEach(doc => {
todos.push({
id: doc.id,
text: doc.data().text,
isCompleted: doc.data().isCompleted,
})
})
return todos
Here is a simple example of the Todo App, but the logic is the same. In the Todo component, I get the doc id as props. Full code:
import { collection, getDocs } from 'firebase/firestore'
import { useEffect, useState } from 'react'
import { db } from '../../app/firebase'
import Todo from '../Todo'
import './style.css'
const TodoList = () => {
const [todos, setTodos] = useState()
useEffect(() => {
const getData = async () => {
const querySnapshot = await getDocs(collection(db, 'users/kvv.prof#gmail.com/todos'))
const todos = []
querySnapshot.forEach(doc => {
todos.push({
id: doc.id,
text: doc.data().text,
isCompleted: doc.data().isCompleted,
})
})
return setTodos(todos)
}
getData()
}, [])
return (
<section className="todo-list">
{todos?.map(todo => (
<Todo key={todo.id} id={todo.id} text={todo.text} isCompleted={todo.isCompleted} />
))}
</section>
)
}
export default TodoList
If you use react-firebase-hooks, then you need to use this
I want to add an item on the list from the API without page reload in react js. currently, it works with a page refresh. I have tried with conditional rendering but was unable to make it.how can I make it? when the user click on add button it should instantly add onto the list
here is my code:
import "./App.css";
import React, { useEffect, useState } from "react";
import axios from "axios";
function App() {
const [todo, setTodo] = useState([]);
const [inputText, setInputText] = useState({
id: "",
title: "",
});
function handleChange(e) {
setInputText({
...inputText,
[e.target.name]: e.target.value,
});
}
const [status, setStatus] = useState(false);
async function addItem(e) {
e.preventDefault();
await axios.post("http://localhost:3333/todos", inputText);
setStatus(true);
setInputText("");
}
async function getTodo() {
try {
const todo = await axios.get("http://localhost:3333/todos");
// console.log(todo.data)
setTodo(todo.data);
} catch (error) {
console.log("something is wrong");
}
}
useEffect(() => {
// Update the document title using the browser API
getTodo();
}, []);
return (
<div className="container">
<div className="heading">
<h1>To-Do List</h1>
</div>
<div className="form">
<input onChange={handleChange} type="text" name="title" />
<button onClick={addItem}>
<span>Add</span>
</button>
</div>
<div>
<ul className="user">
{todo.map((todos) => {
const { id, title } = todos;
return <li key={id}>{title}</li>;
})}
</ul>
</div>
</div>
);
}
export default App;
After creating todo with axios.post, axios will return a response object and the newly created todo data object:
async function addItem(e) {
e.preventDefault();
const res = await axios.post("http://localhost:3333/todos", inputText);
console.log("response:", res) // you can log out the res and see the returned data
setTodo([res.data, ...todo]) // Push to the front of existing todo list
setStatus(true);
setInputText("");
}
import React, { useState } from 'react'
import Display from './components/Display';
const App = () => {
const [input,setInput] = useState("");
const getData = async () => {
const myAPI = await fetch(`http://api.openweathermap.org/data/2.5/weather?q=${input}&units=metric&appid=60dfee3eb8199cac3e55af5339fd0761`);
const response = await myAPI.json();
console.log(response); //want to use response as a prop in Display component
}
return(
<div className="container">
<h1>Weather Report</h1>
<Display title={"City Name :"} /> //here
<Display title={"Temperature :"} /> //here
<Display title={"Description :"} /> //here
<input type={input} onChange={e => setInput(e.target.value)} className="input"/>
<button className="btn-style" onClick={getData}>Fetch</button>
</div>
);
}
export default App;
I don't know if I understand you correctly but if I'm right you want to access data returned from your function that is fetching from API, if so you can try this way
import React, { useState, useEffect } from 'react'
import Display from './components/Display';
import axios from 'axios';
const App = () => {
const [input,setInput] = useState("");
const [state, setState] = useState({loading: true, fetchedData: null});
useEffect(() => {
getData();
}, [setState]);
async function getData() {
setState({ loading: true });
const apiUrl = 'http://api.openweathermap.org/data/2.5/weather?q=${input}&units=metric&appid=60dfee3eb8199cac3e55af5339fd0761';
await axios.get(apiUrl).then((repos) => {
const rData = repos.data;
setState({ loading: false, fetchedData: rData });
});
}
return(
state.loading ? <CircularProgress /> : (
<List className={classes.root}>
{ state.fetchedData.map((row) => (
<div className="container">
<h1>Weather Report</h1>
<Display title={"City Name :" + row.cityName } /> //here
<Display title={"Temperature :" + row.temperature} /> //here
<Display title={"Description :" + row.description} /> //here
</div>
)) }
</List>
)
);
}
So i have Search component and whenever the search get submitted and redirected to a home page with results in it. The problem is that after submission my Search component layout disappears.
import React, { useContext, useEffect } from 'react';
import { MovieContext } from '../context/MovieContext';
import { Redirect } from 'react-router-dom';
const Search = () => {
const { search, setSearch, setMovies, setAlert } = useContext(MovieContext);
const searchMovies = async e => {
e.preventDefault();
if (search.query === '') {
setAlert('Please enter something...');
setTimeout(() => {
setAlert('');
}, 5000);
} else {
const response = await fetch(
`https://api.themoviedb.org/3/search/movie?api_key=35f31bc5ec65018dd8090674c49fe3d2&language=en-US&query=${search.query}&include_adult=false`
);
const data = await response.json();
setMovies(data.results);
setSearch({ query: '', redirect: true });
}
};
const updateSearch = e => {
setSearch({ query: e.target.value });
};
if (search.redirect === true) {
return <Redirect to='/' />;
}
return (
<form
className='form-inline justify-content-center w-75'
onSubmit={searchMovies}
>
<input
className='form-control mr-sm-2 w-50'
type='text'
value={search.query}
placeholder='Search for the movie, person, tv show...'
onChange={updateSearch}
/>
<button className='btn btn-outline-light my-2 my-sm-0' type='submit'>
Search
</button>
</form>
);
};
export default Search;
I know what this happens because i return Redirect and not JSX with my layout. And i struggle to make them both render. I would really appreciate your help.
My code works perfectly fine when executing CRUD operations. However, I have a small issue when I was trying to edit or delete my project, which is located in ProjectEdit.js file. I could edit and delete the project and store the updated data to the MongoDB, but when I used history.push in editProject and deleteProject actions to redirect my ProjectEdit/ProjectDetail page to a Project page, the project list still shows the project that I have already edited plus an undefined key project in which I don't know how it exists. However, if I refreshed the page, there would be no problems at all. I think the problem might be Redux store that holds state tree of my application, but I am not sure. Therefore, I was wondering if you guys could help me out.
Here's an undefined project key.
My project repo is here: https://github.com/topkoong/PersonalWebsite
My staging website is the following: https://shielded-springs-57013.herokuapp.com/
Thank you for your time and consideration.
ProjectEdit.js
import _ from 'lodash';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { reduxForm, Field } from 'redux-form';
import { Link } from 'react-router-dom';
import ProjectField from './ProjectField';
import formFields from './formFields';
import * as actions from '../../actions';
import { withRouter } from "react-router-dom";
class ProjectEdit extends Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
const { _id } = this.props.match.params;
this.props.fetchProject(_id);
}
componentWillReceiveProps({ project }) {
if (project) {
const { title, technology, description, creator } = project;
this.setState({ title, technology, description, creator });
}
}
onHandleSubmit = (event) => {
event.preventDefault();
event.stopPropagation();
const { history} = this.props;
this.props.editProject(this.props.match.params._id, this.state, history);
//this.props.history.push("/project");
}
render() {
return(
<div>
<form onSubmit={this.onHandleSubmit}>
<div className="input-field">
<input
value={this.state.title}
onChange={e => this.setState({ title: e.target.value })}
placeholder="Title"
/>
</div>
<div className="input-field">
<input
value={this.state.technology}
onChange={e => this.setState({ technology: e.target.value })}
placeholder="Technology"
/>
</div>
<div className="input-field">
<input
value={this.state.description}
onChange={e => this.setState({ description: e.target.value })}
placeholder="Description"
/>
</div>
<div className="input-field">
<input
value={this.state.creator}
onChange={e => this.setState({ creator: e.target.value })}
placeholder="Creator"
/>
</div>
<Link to="/project" className="red btn-flat white-text">
Cancel
</Link>
<button type="submit" className="teal btn-flat right white-text">
Submit
<i className="material-icons right">done</i>
</button>
</form>
</div>
);
}
}
// ownProps is the prop obj that is going to ProjectDetail component up top.
const mapStateToProps = ({ projects, auth }, ownProps) => {
return { auth, project: projects[ownProps.match.params._id]};
}
export default connect(mapStateToProps, actions)(withRouter(ProjectEdit));
ProjectDetail.js – using this component to display and delete a project
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchProject, deleteProject } from '../../actions';
import { Link } from 'react-router-dom';
import { withRouter } from "react-router-dom";
class ProjectDetail extends Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
const { _id } = this.props.match.params;
this.props.fetchProject(_id);
}
onDeleteClick() {
console.log("Delete Project was clicked");
const { history } = this.props;
this.props.deleteProject(this.props.project._id, history);
}
render() {
const { project } = this.props;
if (!project) {
return <div>Loading...</div>;
}
const { title, technology, description, creator, datePosted } = project;
return (
<div>
<div className="row">
<div className="col s12 m9">
<h3>{title}</h3>
<h5>Technologies used: {technology}</h5>
<div className="card story">
<div className="card-content">
<span className="card-title">{new Date(datePosted).toLocaleDateString()}</span>
{description}
</div>
</div>
</div>
<div className="col s12 m3">
<div className="card center-align">
<div className="card-content">
<span className="card-title">{this.props.auth.displayName}</span>
<img className="circle responsive-img" src={this.props.auth.image}/>
</div>
</div>
</div>
</div>
<div className="row col s12">
<div className="col s6 offset-s1">
<Link className="waves-effect waves-light btn-large" to="/project">Back to project</Link>
</div>
<button className="btn-large red" onClick={() => this.onDeleteClick()}>Delete</button>
</div>
</div>
);
}
}
// ownProps is the prop obj that is going to ProjectDetail component up top.
function mapStateToProps({ projects, auth }, ownProps){
return { auth, project: projects[ownProps.match.params._id] };
}
export default connect(mapStateToProps, { fetchProject, deleteProject })(withRouter(ProjectDetail));
Project.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import ProjectList from './project/ProjectList';
import { Link } from 'react-router-dom';
class Project extends Component {
render() {
return(
<div>
<h2>Project</h2>
<ProjectList />
{this.props.auth ? <div className="fixed-action-btn">
<Link to="/project/new" className="btn-floating btn-large red">
<i className="material-icons">add</i>
</Link>
</div> : ''}
</div>
);
}
}
function mapStateToProps({ auth }) {
return { auth };
}
export default connect(mapStateToProps)(Project);
ProjectList.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchProjects } from '../../actions';
import { Link } from 'react-router-dom';
import _ from 'lodash';
class ProjectList extends Component {
componentDidMount() {
this.props.fetchProjects();
}
renderProjects() {
// return this.props.projects.reverse().map(project => {
return _.map(this.props.projects, project => {
return (
<div className="row" key={project._id}>
<div className="col s12 m6">
<div className="card">
<div className="card-image">
{this.props.auth ? <Link to={`/project/${project._id}/edit`} className="btn-floating halfway-fab waves-effect waves-light red">
<i className="material-icons">edit</i></Link> : ''}
</div>
<div className="card-content">
<span className="card-title">{project.title}</span>
<p>
<b>Technologies used: {project.technology} </b>
</p>
<p>
<b>Creator: {project.creator}</b>
</p>
<p>
<b>Project description: </b>{project.description}
</p>
<p className="right">
<b>Posted on: </b>{new Date(project.datePosted).toLocaleDateString()}
</p>
</div>
<div className="card-action">
<Link to={`/project/${project._id}`}>Read more</Link>
</div>
</div>
</div>
</div>
);
})
}
render() {
return (
<div>
{this.renderProjects()}
</div>
);
}
}
function mapStateToProps({ projects, auth }) {
return { projects, auth };
}
export default connect(mapStateToProps, { fetchProjects })(ProjectList);
Actions
export const editProject = (id, values, history) => async dispatch => {
const res = await axios.put(`/api/projects/${id}/edit`, values);
dispatch({ type: FETCH_PROJECTS, payload: res.data });
history.push('/project');
}
export const deleteProject = (id, history) => async dispatch => {
const res = await axios.delete(`/api/projects/${id}`);
dispatch({ type: FETCH_PROJECTS, payload: res.data });
history.push('/project');
}
Reducers
import mapKeys from 'lodash/mapKeys';
import { FETCH_PROJECT, FETCH_PROJECTS } from '../actions/types';
export default function(state = [], action) {
switch (action.type) {
case FETCH_PROJECTS:
return { ...state, ...mapKeys(action.payload, '_id') };
case FETCH_PROJECT:
const project = action.payload;
return { ...state, [project._id]: project };
default:
return state;
}
}
API
const mongoose = require('mongoose');
const requireLogin = require('../middlewares/requireLogin');
const Project = mongoose.model('projects');
module.exports = app => {
// show single project
app.get('/api/projects/:id', async (req, res) => {
const project = await Project.findOne({
_id: req.params.id
});
res.send(project);
});
// edit single project
app.put('/api/projects/:id/edit', requireLogin, async (req, res) => {
try {
const project = await Project.findOne({
_id: req.params.id
});
project.title = req.body.title;
project.technology = req.body.technology;
project.description = req.body.description;
project.creator = req.body.creator;
project.datePosted = Date.now();
project._user = req.user.id;
await project.save();
res.send(project);
} catch (err) {
res.status(422).send(err);
}
});
// fetch projects
app.get('/api/projects', async (req, res) => {
const projects = await Project.find({ _user: req.user.id });
// const projects = await Project.find({
// creator: "Theerut Foongkiatcharoen"
// });
res.send(projects);
});
// create a new project
app.post('/api/projects', requireLogin, async (req, res) => {
const { title, technology, description, creator } = req.body;
const project = new Project({
title,
technology,
description,
creator,
datePosted: Date.now(),
_user: req.user.id
});
try {
await project.save();
const user = await req.user.save();
res.send(user);
} catch (err) {
res.status(422).send(err);
}
});
// delete a single project
app.delete('/api/projects/:id', requireLogin, async (req, res) => {
try {
const project = await Project.remove({
_id: req.params.id
});
res.sendStatus(200);
} catch (err) {
res.status(422).send(err);
}
});
};
In the code:
const res = await axios.delete(`/api/projects/${id}`);
dispatch({ type: FETCH_PROJECTS, payload: res.data }.
Isn't this API endpoint returning only a HTTP status code, which you are then sending to reducer?
This explains your undefined key. Its undefined because you set it to non existing data from response.