React UseEffect not updating when information is deleted from array - javascript

I'm looking to get some help as I've been stuck on this for a while. I have a MERN stack application interacting with the Github API. Users are able to search for Github users and save them to their profile on the app and once they do that they will also start following the user on Github. In the same fashion users are able to delete the users from their profile in the app and that will unfollow the same user on Github. The problem is that when I click on the delete user button, it does not update on the browser unless I refresh the page then I see that the user is deleted from the array of saved users. In the Profile.jsx component I am rendering the list of saved users and fetching the information from the database. In the SavedGithubUsersCard.jsx component I am mapping through the saved users data to render the user's information such as name. In DeleteButton.jsx I am deleting the specific saved user when I click on the button. My question is what am I doing wrong? is the prop change not being fired or is there another issue with UseEffect?
Here is the code:
import React, { useEffect, useContext } from 'react';
import swal from 'sweetalert';
import { AppContext } from '../context/AppContext';
import SavedGithubUsersCard from './SavedGithubUsersCard';
import axios from 'axios';
Profile.jsx
const Profile = () => {
const {
githubUserData, setGithubUserData
} = useContext(AppContext);
useEffect(() => {
axios.get('/api/githubdata').then((res) => {
setGithubUserData(res.data);
})
.catch((err) => {
if (err) {
swal('Error', 'Something went wrong.', 'error');
}
});
}, [setGithubUserData]);
return (
<div className="profile-saved-users">
<div className="githubUserData-cards">
<h1 className="saved-users-header">Saved users</h1>
{!githubUserData || githubUserData.length === 0 ? (
<p className="no-saved-users-text">No saved users yet :(</p>
) : (
<SavedGithubUsersCard githubUserData={githubUserData}/>
)}
</div>
</div>
);
};
export default Profile;
import React from 'react';
import { Card, Button } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import DeleteButton from './DeleteButton';
SavedGithubUsersCard.jsx
const SavedGithubUsersCard = ({githubUserData}) => {
return (
<>
{githubUserData?.map(githubUserData => (
<Card key={githubUserData._id} id="saved-users-card">
<Card.Img
variant="top"
src={githubUserData.avatar_url}
id="saved-users-card-image"
/>
<Card.Body id="saved-users-card-information">
<Card.Title
id="saved-users-name"
style={{ textAlign: 'center' }}
>
{githubUserData.name}
</Card.Title>
<Card.Subtitle
id="saved-users-username"
className="mb-2 text-muted"
style={{ textAlign: 'center' }}
>
{githubUserData.login}
</Card.Subtitle>
<Card.Text id="saved-users-profile-url">
Profile URL: {githubUserData.html_url}
</Card.Text>
<Link
to={{ pathname: "githubUserData.html_url" }}
target="_blank"
>
<Button
id="saved-users-profile-button"
variant="outline-primary"
>
View profile
</Button>
</Link>
<DeleteButton githubUserDataId={githubUserData._id} githubUserDataLogin= {githubUserData.login} />
</Card.Body>
</Card>
))}
</>
);
};
export default SavedGithubUsersCard
DeleteButton.jsx
import React from 'react';
import { Button } from 'react-bootstrap';
import axios from 'axios';
import swal from 'sweetalert';
const DeleteButton = ({ githubUserDataLogin, githubUserDataId }) => {
const handleRemove = async () => {
try {
fetch(`https://api.github.com/user/following/${githubUserDataLogin}`, {
method: 'DELETE',
headers: {
Authorization: `token ${process.env.GITHUB_TOKEN}`
}
});
await axios({
method: 'DELETE',
url: `/api/githubdata/${githubUserDataId}`,
withCredentials: true
});
swal(
'Removed from profile!',
`You are no longer following ${githubUserDataLogin} on Github`,
'success'
);
} catch (err) {
swal('Error', 'Something went wrong.', 'error');
}
};
return (
<Button
variant="outline-danger"
style={{ marginLeft: 20 }}
onClick={handleRemove}
id="saved-users-delete-button"
>
Remove User
</Button>
);
};
export default DeleteButton;
AppContext.jsx
import React, { createContext, useState, useEffect } from 'react';
import axios from 'axios';
const AppContext = createContext();
const AppContextProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null);
const [loading, setLoading] = useState(false);
const [githubUserData, setGithubUserData] = useState([]);
const user = sessionStorage.getItem('user');
useEffect(() => {
// incase user refreshes and context is cleared.
if (user && !currentUser) {
axios
.get(`/api/users/me`, {
withCredentials: true
})
.then(({ data }) => {
setCurrentUser(data);
})
.catch((error) => console.error(error));
}
}, [currentUser, user]);
return (
<AppContext.Provider
value={{ currentUser, setCurrentUser, loading, setLoading, githubUserData, setGithubUserData}}
>
{children}
</AppContext.Provider>
);
};
export { AppContext, AppContextProvider };
Thank you!

From what I see of your code when you delete a user you successfully delete them in the back end but don't also update the local state.
Option 1 - Refetch the users list and update local state
const DeleteButton = ({ githubUserDataLogin, githubUserDataId }) => {
const { setGithubUserData } = useContext(AppContext);
const handleRemove = async () => {
try {
fetch(....);
await axios(....);
swal(....);
const usersDataRes = await axios.get('/api/githubdata');
setGithubUserData(usersDataRes.data);
} catch (err) {
swal('Error', 'Something went wrong.', 'error');
}
};
return (
...
);
};
Option 2 - Attempt to manually synchronize the local state
const DeleteButton = ({ githubUserDataLogin, githubUserDataId }) => {
const { setGithubUserData } = useContext(AppContext);
const handleRemove = async () => {
try {
fetch(....);
await axios(....);
swal(....);
setGithubUserData(users =>
users.filter(user => user._id !== githubUserDataId)
);
} catch (err) {
swal('Error', 'Something went wrong.', 'error');
}
};
return (
...
);
};

Related

React social media app reverse post order

I made a social media app and this is my homepage
import { Box, useMediaQuery } from "#mui/material";
import { useSelector } from "react-redux";
import Navbar from "scenes/navbar";
import UserWidget from "scenes/widgets/UserWidget";
import MyPostWidget from "scenes/widgets/MyPostWidget";
import PostsWidget from "scenes/widgets/PostsWidget";
import AdvertWidget from "scenes/widgets/AdvertWidget";
import FriendListWidget from "scenes/widgets/FriendListWidget";
const HomePage = () => {
const isNonMobileScreens = useMediaQuery("(min-width:1000px)");
const { _id, picturePath } = useSelector((state) => state.user);
return (
<Box>
<Navbar />
<Box
width="100%"
padding="2rem 6%"
display={isNonMobileScreens ? "flex" : "block"}
gap="0.5rem"
justifyContent="space-between"
>
<Box flexBasis={isNonMobileScreens ? "26%" : undefined}>
<UserWidget userId={_id} picturePath={picturePath} />
</Box>
<Box
flexBasis={isNonMobileScreens ? "42%" : undefined}
mt={isNonMobileScreens ? undefined : "2rem"}
>
<MyPostWidget picturePath={picturePath} />
<PostsWidget userId={_id} />
</Box>
{isNonMobileScreens && (
<Box flexBasis="26%">
<AdvertWidget />
<Box m="2rem 0" />
<FriendListWidget userId={_id} />
</Box>
)}
</Box>
</Box>
);
};
export default HomePage;
and this is my post widget
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { setPosts } from "state";
import PostWidget from "./PostWidget";
const PostsWidget = ({ userId, isProfile = false }) => {
const dispatch = useDispatch();
const posts = useSelector((state) => state.posts);
const token = useSelector((state) => state.token);
const getPosts = async () => {
const response = await fetch("http://localhost:3001/posts", {
method: "GET",
headers: { Authorization: `Bearer ${token}` },
});
const data = await response.json();
dispatch(setPosts({ posts: data }));
};
const getUserPosts = async () => {
const response = await fetch(
`http://localhost:3001/posts/${userId}/posts`,
{
method: "GET",
headers: { Authorization: `Bearer ${token}` },
}
);
const data = await response.json();
dispatch(setPosts({ posts: data }));
};
useEffect(() => {
if (isProfile) {
getUserPosts();
} else {
getPosts();
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
return (
<>
{posts.map(
({
_id,
userId,
firstName,
lastName,
description,
location,
picturePath,
userPicturePath,
likes,
comments,
createdAt,
}) => (
<PostWidget
key={_id}
postId={_id}
postUserId={userId}
name={`${firstName} ${lastName}`}
description={description}
location={location}
picturePath={picturePath}
userPicturePath={userPicturePath}
likes={likes}
comments={comments}
createdAt={createdAt} // Pass the createdAt field as a prop to the PostWidget component
/>
)
)}
</>
);
};
export default PostsWidget;
when I post new post in my page old posts are staying on top and new posts are going below of the page how can I fix this. its my oldest old post post but it is top . I use mongodb and my posts hase timestamps I want to order with timestamps

Getting component to re-render after form submit. Using useContext and custom hooks to fetch data

I'm having some issues getting a component to re-render after submitting a form. I created separate files to store these custom hooks to make them as reusable as possible. Everything is functioning correctly, except I haven't figured out a way to re render a list component after posting a new submit to that list. I am using axios for fetch requests and react-final-form for my actual form. Am I not able to re-render the component because I am using useContext to "share" my data across child components? My comments are set up as nested attributes to each post, which is being handled through Rails. My comment list is rendered in it's own component, where I call on the usePost() function in the PostContext.js file. I can provide more info/context if needed.
**
Also, on a slightly different note. I am having difficulty clearing the form inputs after a successful submit. I'm using react-final-form and most the suggestions I've seen online are for class components. Is there a solution for functional components?
react/contexts/PostContext.js
import React, { useContext, useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { useAsync } from "../hooks/useAsync";
import { getPost } from "../services/post";
const Context = React.createContext();
export const usePost = () => {
return useContext(Context);
};
export const PostProvider = ({ children }) => {
const id = useParams();
const { loading, error, value: post } = useAsync(() => getPost(id.id), [
id.id,
]);
const [comments, setComments] = useState([]);
useEffect(() => {
if (post?.comments == null) return;
setComments(post.comments);
}, [post?.comments]);
return (
<Context.Provider
value={{
post: { id, ...post },
comments: comments,
}}
>
{loading ? <h1>Loading</h1> : error ? <h1>{error}</h1> : children}
</Context.Provider>
);
};
react/services/comment.js
import { makeRequest } from "./makeRequest";
export const createComment = ({ message, postId }) => {
message["post_id"] = postId;
return makeRequest("/comments", {
method: "POST",
data: message,
}).then((res) => {
if (res.error) return alert(res.error);
});
};
react/services/makeRequest.js
import axios from "axios";
const api = axios.create({
baseURL: "/api/v1",
withCredentials: true,
});
export const makeRequest = (url, options) => {
return api(url, options)
.then((res) => res.data)
.catch((err) => Promise.reject(err?.response?.data?.message ?? "Error"));
};
react/components/Comment/CommentForm.js
import React from "react";
import { Form, Field } from "react-final-form";
import { usePost } from "../../contexts/PostContext";
import { createComment } from "../../services/comment";
import { useAsyncFn } from "../../hooks/useAsync";
const CommentForm = () => {
const { post, createLocalComment } = usePost();
const { loading, error, execute: createCommentFn } = useAsyncFn(
createComment
);
const onCommentCreate = (message) => {
return createCommentFn({ message, postId: post.id });
};
const handleSubmit = (values) => {
onCommentCreate(values);
};
return (
<Form onSubmit={handleSubmit}>
{({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<div className="comment-form-row">
<Field name="body">
{({ input }) => (
<textarea
className="comment-input"
placeholder="Your comment..."
type="text"
{...input}
/>
)}
</Field>
<button className="comment-submit-btn" type="submit">
Submit
</button>
</div>
</form>
)}
</Form>
);
};
export default CommentForm;

State is null, after useEffect hooks

I am trying to set product by dispatching a method in a useEffect. However, state still says null.
index.html
import React, { useEffect, Fragment } from "react";
import { useSelector, useDispatch } from "react-redux";
import { fetchProductsData } from "../../store/products-actions";
import Promotion from "./components/Promotion";
import Products from "./components/Products";
import ToastUi from "../../shared/ui/ToastUi";
import { Container, Row, Col } from "react-bootstrap";
const Home = () => {
const dispatch = useDispatch();
// const products = useSelector((state) => state.products.products);
const products = useSelector((state) => state.products.productsTest);
const cartQuantity = useSelector((state) => state.cart.quantity);
useEffect(() => {
dispatch(fetchProductsData());
}, [dispatch]);
return (
<Fragment>
<ToastUi
status="Sukses"
title="Notifikasi"
message={`(${cartQuantity}) produk baru berhasil di masukkan keranjang`}
/>
<Container fluid="xl">
<Row>
<Col>
<Promotion />
</Col>
</Row>
<Row className="mt-3" md={3}>
<Products products={products} />
</Row>
</Container>
</Fragment>
);
};
export default Home;
Products still says null after a cycle, apparently it needs second cycle to make that state changed. Not sure how can I make it change in one cycle. Do I need to put the useEffect in the parent ?
EDIT
if I add this, it will work
{products !== null && <Products products={products} />}
// {/* <Products products={products} /> */} //
However, is there a better way or maybe some explanation on why this is happening, Thank you.
EDIT
products-slice.js
import { createSlice } from "#reduxjs/toolkit";
import {
products,
excel_products,
product,
filteredProducts,
productsTest,
} from "../datafiles";
const initialProductsState = {
products,
excel_products,
product,
filteredProducts,
productsTest,
};
const productsSlice = createSlice({
name: "products",
initialState: initialProductsState,
reducers: {
viewExcelProducts(state, action) {
state.excel_products = action.payload;
},
uploadExcelProducts(state) {
if (excel_products.length < 0) {
console.log("error");
} else {
const newProducts = state.products.concat(state.excel_products);
state.products = newProducts;
state.excel_products = [];
}
},
selectProduct(state, action) {
const product = state.products.find((item) => item.id === action.payload);
state.product = product;
},
filterProducts(state, action) {
const filteredProducts = state.products.filter(
(item) => item.type === action.payload
);
state.filteredProducts = filteredProducts;
},
setProducts(state, action) {
state.productsTest = action.payload;
},
},
});
export const productsActions = productsSlice.actions;
export default productsSlice;
products-actions.js
import { productsActions } from "./products-slice";
export const fetchProductsData = () => {
return async (dispatch) => {
const fetchData = async () => {
const response = await fetch("http://localhost:5000/products");
if (!response.ok) {
throw new Error("Could not fetch data!");
}
const data = await response.json();
return data;
};
try {
const productsData = await fetchData();
dispatch(productsActions.setProducts(productsData));
} catch (err) {
console.log("Error: " + err);
}
};
};
What do you mean by it needs second cycle to make that state changed?
fetchProductsData is an async function, I assume. That means that you do not receive data immediately, but after some time (depending on network connection speed, payload size etc). So it is OK that your data arrives later.
Usual approach for async data is to keep isLoading in your state. And use it as following:
const isLoading = useSelector((state) => state.products.isLoading);
...
return (
<Fragment>
...
{isLoading && <Spinner />} // Some loading indicator
{!isLoading && <Products products={products} />}
</Fragment>
);
This way you will indicate to user that some data is being fetched. This is a good UX approach.
isLoading should be set somewhere in your fetchProductsData action, like so:
export const fetchProductsData = () => {
return async (dispatch) => {
...
try {
dispatch(productsActions.setIsLoading(true));
const productsData = await fetchData();
dispatch(productsActions.setProducts(productsData));
} catch (err) {
console.log("Error: " + err);
} finally {
dispatch(productsActions.setIsLoading(false));
}
};
};

useEffect called multiple times when using <Redirect> or history.push()

I got a little problem because I can't redirect logged in user to app, when he's saved in localStorage.
Both react-router-dom functions return Maximum update depth exceeded. but why?
import React, { useState, useEffect } from 'react'
import { authLocalUser } from 'actions/userActions'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { useHistory } from 'react-router-dom'
// components
import SigninForm from 'components/organisms/Forms/SigninForm'
import SignupForm from 'components/organisms/Forms/SignupForm'
// styles
import { Content, Footer, Wrapper, Header } from './styles'
const Landing = ({ fetchLocalStorage, userID }) => {
const [isModalOpen, setModalOpen] = useState(false)
const history = useHistory()
useEffect(async () => {
const userData = await JSON.parse(
localStorage.getItem('userData'),
)
await fetchLocalStorage(userData)
}, [])
return (
<>
{userID && history.push('/app')}
<Content>
{isModalOpen && (
<div
style={{
zIndex: 300,
left: 0,
position: 'absolute',
width: '100%',
height: '100%',
background: 'rgb(0,0,0,0.5)',
}}
>
<SignupForm setModalOpen={setModalOpen} />
</div>
)}
<Wrapper w='60'>
<Header>
<h1>ChatterApp</h1>
<h3>
Chat with your friend in real-time using
magic of Web Sockets! Join our community
today!
</h3>
</Header>
</Wrapper>
<Wrapper signin w='40'>
<SigninForm setModalOpen={setModalOpen} />
</Wrapper>
</Content>
<Footer>
<Content>
<h3>ChatterApp</h3>
<h5>Dawid Szemborowski</h5>
</Content>
</Footer>
</>
)
}
const mapStateToProps = ({ user }) => ({
userID: user._id,
})
const mapDispatchToProps = dispatch => ({
fetchLocalStorage: localStorage =>
dispatch(authLocalUser(localStorage)),
})
export default connect(mapStateToProps, mapDispatchToProps)(Landing)
Landing.propTypes = {
userID: PropTypes.string,
fetchLocalStorage: PropTypes.func.isRequired,
}
Landing.defaultProps = {
userID: undefined,
}
I tried calling this function without async/await, I tried providing userID and localStorage as that last parameter for componentDidUpdate. Where is my problem? Error I get displays the problem is inside Lifecycle component
index.js:1 The above error occurred in the <Lifecycle> component:
at Lifecycle (http://localhost:3000/static/js/vendors~main.chunk.js:47761:29)
at Redirect (http://localhost:3000/static/js/vendors~main.chunk.js:47862:28)
authLocalUser code
export const authLocalUser = userData => {
return {
type: 'FETCH_LOCAL_STORAGE',
payload: userData,
}
}
You probably want to do something like this.
Replacing {userID && history.push('/app')} with:
useEffect(() => {
if(userId) {
history.push('/app')
}
}, [userId])
As a suggestion, your first useEffect call can be corrected. If you make the callback of useEffect as async it will return a promise which is not the way useEffect works. It returns a cleanup function.
Use an IIFE instead:
useEffect(() => {
(async () => {
const userData = JSON.parse(localStorage.getItem('userData'))
await fetchLocalStorage(userData)
})()
}, [])

How to pass id from one component to another component onclick of an element

I'm trying to pass this is as id as props to another component which is not a child of the component. I was considering using context but i wanted to know if there was another way to it, since I'm quite new to react I'm looking for a more efficient way.
This is the component where the id of the element clicked is being generated. When i logged it the data is correct an no problems was notified. I first tried passing it as props as seen below but since i didn't want it to be seen on that page i didn't pass it to the main return statement neither did i call the method in it, but then it returned undefined in the component where i wanted to make use of it
import React, { useState } from 'react'
import { useHistory } from 'react-router-dom';
import Workspacelist from '../Workspace/Workspacelist';
function BoardList({ boards }) {
const [currentid, setcurrentid] = useState('')
const history = useHistory()
const navigate = (id) => {
setcurrentid(id);
console.log(id)
history.push(`/workspace/${id}`)
return(
<Workspacelist id = {id}/>
)
}
return (
<>
{
boards.map((board) => (
<li key={board.id} className="boardlist" style={styles} onClick={() => navigate(board.id)}>
<h3>{board.title}</h3>
</li>
))}
</>
)
}
export default BoardList
PS: Firebase is being incoporated in this project, i was thinking that might be the reason cause it's my first time using firebase so maybe I'm missing something since all the data is coming from the server
And this is the component i want to pass it to
import React, { useState, useEffect } from 'react'
import Firebase, { db } from '../Firebase/Firebase';
import { Todo } from './List';
function Workspacelist({ id }) {
const [updatedId] = useState(id)
const [show, setshow] = useState(false);
const [Todos, setTodos] = useState([]);//Todolist
const [ToDo, setToDo] = useState('');
useEffect(() => {
const docRef = db.collection("boards").doc(updatedId).get().then(doc => {
if (doc.exists) {
setTodos(doc.data().todo);
console.log("Document data:", doc.data().todo);
} else {
console.log("No such document!");
}
}).catch(function (error) {
console.log("Error getting document:", error);
});
return docRef
})
return (
<div className="workspacelist">
<div className="todo">
<div>
<b>To Do</b>
<b>...</b>
<Todo Todos={Todos} />
<span onClick={() => { setshow(current => !current) }} style={{ display: show ? 'none' : 'block' }}>+ Add a card</span>
</div>
<div className="add" style={{ display: show ? 'block' : 'none' }}>
<textarea placeholder="Enter a title for this card..." value={ToDo} onChange={(e) => { setToDo(e.target.value) }} />
<button className="addcard" onClick={one}>Add Card</button>
<button onClick={() => { setshow(current => !current) }}>X</button>
<button className="more">...</button>
</div>
</div>
</div>
)
}
export default Workspacelist
Thanks in advance i did appreciate the help even if i have to rewrite it just tell me the way you would do it if you were in my shoes
To navigate to another page, you just need history.push(/workspace/${id}).
You don't even need any state here.
import React, { useState } from 'react'
import { useHistory } from 'react-router-dom';
import Workspacelist from '../Workspace/Workspacelist';
function BoardList({ boards }) {
const history = useHistory()
const navigate = (id) => {
history.push(`/workspace/${id}`)
}
return (
<>
{
boards.map((board) => (
<li key={board.id} className="boardlist" style={styles} onClick={() => navigate(board.id)}>
<h3>{board.title}</h3>
</li>
))}
</>
)
}
export default BoardList
To get the id param on the Workspace page, you will need to use the useRouteMatch hook from react-router-dom:
import { useRouteMatch } from 'react-router-dom';
function Workspacelist() {
const {
params: { id },
} = useRouteMatch('/workspace/:id');
console.log(id)
}
Let me know if it solves your problem.
If you use dom version 6, change the following parts that showed in #HichamELBSI answer.
useHistory should change into useNavigate.
useRouteMatch should change into useMatch.
After applying those, the codes should be
import { useNavigate} from 'react-router-dom';
const nav = useNavigate();
const navigate = (id) => {
nav(`/workspace/${id}`)
}
Then other part should be
import { useMatch } from 'react-router-dom';
function Workspacelist() {
const {
params: { id },
} = useMatch('/workspace/:id');
console.log(id)
}

Categories

Resources