Problem with asynchronous tasks in useEffect hook - reactjs

I have an error like this:
index.js:1 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
at ToDoAdder (http://localhost:3000/static/js/main.chunk.js:193:3)
here's my code:
import React, { useState } from 'react';
import { useEffect } from 'react';
import { Trash, Pen, CheckLg, XCircleFill } from 'react-bootstrap-icons';
import { Button } from 'react-bootstrap';
import store from '../../store';
function ToDoAdder({ data }) {
const [user, setUser] = useState({});
const { id, title, completed, userId } = data;
useEffect(() => {
fetchUsers();
}, [user]);
const fetchUsers = async () => {
const data = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
const users = await data.json();
setUser(users);
return user;
};
const handleTodoRemove = () => {
console.log(id);
store.dispatch({ type: 'remove/todo', payload: { id } });
};
return (
// here goes some code
)
}
export default ToDoAdder;
Please help me to fix this problem

That's not an error, that's a warning.
As it says, your component unmounted (why, we can't tell, since we don't see how it's used) before the async call finished.
In your case, it doesn't even indicate a memory leak unlike the message insinuates.
If you want to get rid of the warning, you can guard the setUser call using a hook that checks whether the component is actually still mounted at that point. One such reusable hook is react-use's useMountedState.

Related

Custom React-native Hook runs the component that calls it twice. I don't understand why

I am trying to learn to work with custom Hooks in React-native. I am using AWS Amplify as my backend, and it has a method to get the authenticated user's information, namely the Auth.currentUserInfo method. However, what it returns is an object and I want to make a custom Hook to both returns the part of the object that I need, and also abstract away this part of my code from the visualization part. I have a component called App, and a custom Hook called useUserId. The code for them is as follows:
The useUserId Hook:
import React, { useState, useEffect } from "react";
import { Auth } from "aws-amplify";
const getUserInfo = async () => {
try {
const userInfo = await Auth.currentUserInfo();
const userId = userInfo?.attributes?.sub;
return userId;
} catch (e) {
console.log("Failed to get the AuthUserId", e);
}
};
const useUserId = () => {
const [id, setId] = useState("");
const userId = getUserInfo();
useEffect(() => {
userId.then((userId) => {
setId(userId);
});
}, [userId]);
return id;
};
export default useUserId;
The App component:
import React from "react";
import useUserId from "../custom-hooks/UseUserId";
const App = () => {
const authUserId = useUserId();
console.log(authUserId);
However, when I try to run the App component, I get the same Id written on the screen twice, meaning that the App component is executed again.
The problem with this is that I am using this custom Hook in another custom Hook, let's call it useFetchData that fetches some data based on the userId, then each time this is executed that is also re-executed, which causes some problems.
I am kind of new to React, would you please guide me on what I am doing wrong here, and what is the solution to this problem. Thank you.
The issue is likely due to the fact that you've declared userId in the hook body. When useUserId is called in the App component it declares userId and updates state. This triggers a rerender and userId is declared again, and updates the state again, this time with the same value. The useState hook being updated to the same value a second time quits the loop.
Bailing out of a state update
If you update a State Hook to the same value as the current state,
React will bail out without rendering the children or firing effects.
(React uses the Object.is comparison algorithm.)
Either move const userId = getUserInfo(); out of the useUserId hook
const userId = getUserInfo();
const useUserId = () => {
const [id, setId] = useState("");
useEffect(() => {
userId.then((userId) => {
setId(userId);
});
}, []);
return id;
};
or more it into the useEffect callback body.
const useUserId = () => {
const [id, setId] = useState("");
useEffect(() => {
getUserInfo().then((userId) => {
setId(userId);
});
}, []);
return id;
};
and in both cases remove userId as a dependency of the useEffect hook.
Replace userId.then with to getUserId().then. It doesn't make sense to have the result of getUserId in the body of a component, since it's a promise and that code will be run every time the component renders.

Avoid update on an unmounted component

I read a lot of similar questions but it seems my questions is a little different.
I am trying to login and I get the following error.
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect
cleanup function.
I am attaching the AuthContext.js that handles all the logic hoping you can explain me what is wrong and what knowledge it indicates I need to learn.
import React, { useContext, useState } from 'react'
import {postData} from '../adapters/authAdapter';
const AuthContext = React.createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({children}) {
const [currentUser,setCurrentUser] = useState(null);
async function login(email,password) {
const adaptedRes = await postData('log-in',{email,password});
if(adaptedRes.err) {
throw new Error(adaptedRes.message)
} else {
return setCurrentUser(adaptedRes.data);
}
}
const value = {
currentUser,
login
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
Thank you in advance
The error means that your setCurrentUser() function is being called, when AuthProvider is not currently mounted. That's why, use useRef() to check if your AuthProvider is mounted, and then set state as mentioned in the link:
export function AuthProvider({children}) {
const [currentUser,setCurrentUser] = useState(null);
const isMounted = useRef(false);
useEffect(() => {
// Becomes true on component mount
isMounted.current = true;
// becomes false on unmount
return () => {isMounted.current = false;}
}, [])
async function login(email,password) {
const adaptedRes = await postData('log-in',{email,password});
if(adaptedRes.err) {
throw new Error(adaptedRes.message)
} else {
if(!isMounted.current) {
// Check if component is mounted
console.log('component is not mounted');
return;
}
return setCurrentUser(adaptedRes.data);
}
}
const value = {
currentUser,
login
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
Update As Drew Pointed out:
Issue is as others say, the AuthProvider component is being unmounted before setCurrentUser is called. Instead of putting a band-aid fix in to keep the state update from occurring you should figure out why your auth provider isn't mounted. This is one of those provider components you typically want wrapping your entire app and mounted at all times. My guess is you've narrowed the scope of this auth provider too much to just around your auth component UI and then you navigate elsewhere and the provider is no longer available.
So, you should also check this, as if you are getting this error, then there is implementation problem somewhere.

Redux useSelector not updated, need to be refresh

i face this issue.
when i have a Axios call, which promise to dispatch a action to update the redux and execute the callback.
but when callback is executed, the redux state seem to be stale.
i got a sandbox code for demo here
if you click on the getNewDate Button, the console will show the difference in the redux state.
the state will be correct when redux cause a re-render.
How do i get the correct redux state during callback?
The response will always be stale, that's how React hooks work. They apply a closure over all the variables in each individual render when they are created. If you absolutely need the value to not be stale in a callback function (or effect), set up a ref for it.
const { response, getNewDate } = useResponse();
const responseRef = useRef(response);
useLayoutEffect(() => {
responseRef.current = response;
}, [response]);
const callbackSuccessful = (data: IResponse) => {
console.log("response is not Stale: " + responseRef.current.newDate);
console.log("should be: " + data.newDate);
};
Once you set up a ref, you'll clearly see that the response is in-fact changing and the responseRef.current shows the same value as data.newDate.
You have to useLayoutEffect here because the order in which the effect runs is wrong for the callback. Since useEffect runs after the component re-renders while useLayoutEffect runs while the component re-renders.
Another way you could see that the useSelector is working fine and updating and that your MyPages.tsx is seeing that update is useEffect to log the change whenever it changes.
useEffect(() => {
console.log(response.newDate)
}, [response]);
If you want access to the latest redux store state in a callback without any timing issues at all, useStore is helpful, and it doesn't cause re-rendering at all.
const store = useStore();
const callbackSuccessful = (data: IResponse) => {
console.log("should be: " + data.newDate);
console.log("Redux store: " + store.getState().apiResponse.newDate);
};
https://codesandbox.io/s/react-typescript-demo-pek65?file=/src/pages/MyPages.tsx
The use previous answer of Zachary Haber's useLayoutEffect is the correct answer.
But here are two subpar solutions that I can share, both with their own issues.
Solution 1
Use a key on the button to inform React that it should renew the scope of the button object (i.e. re-mount the component) because some of it's dependencies have updated.
import React from "react";
import { useResponse } from "../hooks/useResponse";
import { IResponse } from "../utils/apiDef";
const MyPages = () => {
const { response, getNewDate } = useResponse();
const callbackSuccessful = (data: IResponse) => {
console.log("response is: " + response.newDate)
console.log("callback data is: " + data.newDate)
}
const callbackFail = (data: any) => {
}
const handleButton = () => {
getNewDate(callbackSuccessful, callbackFail)
}
const buttonKey = response.newDate
return <div>
hello world {response.newDate}
<br/>
<button key={buttonKey} type="button" onClick={handleButton}>getNewDate</button>
</div>;
};
export default MyPages;
A slight problem: This will display the previous result, i.e. response.newDate will contain the value it had when the key was changed.
Solution 2
Use a global variable e.g. state
import React from "react";
import { useResponse } from "../hooks/useResponse";
import { IResponse } from "../utils/apiDef";
const state = {
response: undefined
}
const MyPages = () => {
const { response, getNewDate } = useResponse();
state.response = response;
const callbackSuccessful = (data: IResponse) => {
console.log("state response is: " + state.response.newDate)
console.log("callback data is: " + data.newDate)
}
const callbackFail = (data: any) => {
}
const handleButton = () => {
getNewDate(callbackSuccessful, callbackFail)
}
return <div>
hello world {response.newDate}
<br/>
<button type="button" onClick={handleButton}>getNewDate</button>
</div>;
};
export default MyPages;
In the call back the state.response.newDate is equal to data.newDate.
A slight problem: It's not a reusable component anymore. This solution will only work if you ever have one MyPages object in your app. All instances will point to the same static global variable and this will introduce a contention of whoever writes last wins.
You should not do:
<MyPages />
<MyPages />
Another issue I have with this solution: React's optimization; I don't know how this will affect it.
I hope this helps.

useEffect goes in infinite loop when combined useDispatch, useSelector of Redux

I try to code with react-hook and redux for state management and axios for database requests with Thunk as middleware for handling asynchronicity.I'm having an issue in one component that does a get request to retrieve a list of customers on what used to be componentwillreceiveprop
# Action
export const actGetProductRequest = id => dispatch =>
callApi(`products/${id}`, "GET").then(res =>
dispatch({
type: types.EDIT_PRODUCT,
payload: { product: res.data }
})
);
------
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import useForm from "./useForm";
import { actGetProductRequest } from "../../actions";
...
const { history, match } = props;
const getProduct = useSelector(state => state.itemEditing);
const dispatch = useDispatch();
const { values, setValues, handleChange, handleSubmit } = useForm(
getProduct,
history
);
useEffect(() => {
if (match) {
let id = match.params.id;
dispatch(actGetProductRequest(id));
}
setValues(()=>({...getProduct}));
},[match,dispatch, setValues, getProduct]);
And I tried to call API and return the product, but the site always loop render infinite. Continuous rendering cannot be edited the form. like the
Can anyone please help me out how to resolve this issue...
p.s: with code here, it run ok. But I want to use with redux, and I pass the code calling axios to the dispatch and redux to return the new changed state
useEffect(() => {
if (match) {
let id = match.params.id;
callApi(`products/${id}`, "GET", null).then(res => {
const data = res.data;
setValues(()=>({...data}));
});
}
const clearnUp = () => setValues(false);
return clearnUp;
},[match, setValues]);
p.s: full code
code
Just get the id from the history > match
const { history, match } = props;
const id = match ? match.params.id : null;
You don't need to add dispatch to the dependencies. Instead of match, use id. And you can skip the method references. Since setValues is basically a setState call, it can be skipped too (Read React docs). But if you do want to use any function references in dependecies, make sure you wrap them with useCallback.
useEffect(() => {
if (id) {
dispatch(actGetProductRequest(id));
}
setValues(()=>({...getProduct}));
},[id, getProduct]);
Your main issue might be with the match object. Since, history keeps changing, even if the id is the same.

Updating component state inside of useEffect

I'm playing around w/ hooks for the first time. In my component I make a request for some data inside of useEffect then want to save that data to local state. I try using setUser inside of useEffect but state never gets updated. I've tried it w/o the second argument [] but I get an infinite loop. I know setState is asynchronous and previously you could specify a function as a second argument to setState which would run when state had updated.
I'm wondering what the correct process is for updating state inside of useEffect
import React, { useEffect, useState } from 'react';
import { withRouter } from 'react-router-dom';
import { db } from '../../constants/firebase';
function Profile(props) {
const [ user, setUser ] = useState(null);
const { username } = props.match.params;
useEffect(() => {
db.collection("users").where("username", "==", username)
.get()
.then((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(doc.data()); // data comes back fine
setUser(doc.data());
console.log(user); // user is still null
});
})
.catch((error) => {
console.log("Error getting user: ", error);
});
}, [username])
return <div>Profile</div>
}
export default withRouter(Profile);
You can use another useEffect which will run when user updates.
useEffect(() => {
console.log(user);
...
}, [user]);
Try change 2nd argument of useEffect from [username] to [props.match.params.username]

Resources