How to fix the unmounted component in react hooks - reactjs

How to fix the 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.
here' the code:
useEffect(() => {
let ignore = false;
setTimeout(() => {
const fetchData = async () => {
try {
setLoading(true);
setError({});
const response = await getLatest();
if (!ignore) setData(response['data']);
} catch (err) {
setError(err);
}
setLoading(false);
};
fetchData();
});
return () => {
ignore = true;
};
}, []);
The problem here is when I click the home page while loading the data and click the room page. then the error will appeared. How to fix on it?

You need to clean up your timeout on unmount, otherwise, it tries to execute your hooks to update the internal state of the component after it goes out of scope (I assume functions like setLoading and setError are from a React hook).
To do so, put the output of setTimeout in a variable and then call clearTimeout in the function you return from useEffect. That's the clean up function and it's run when the component gets unmounted. So you need something like this:
React.useEffect(() => {
...
const timeout = setTimeout(...)
return () => {
...
clearTimeout(timeout);
}
}, [])
See the docs for more info: https://reactjs.org/docs/hooks-reference.html#cleaning-up-an-effect

Related

Trying to implement a cleanup in a useEffect to prevent no-op memory leak error

I am trying to update a piece of UI based on a conditional. The conditional is set by a database call in a separate component. It sometimes works, but often doesn't. When it doesn't work, it gets this 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 have followed other advice and tried to do a cleanup in a useEffect:
const [isPatched, setIsPatched] = useState<boolean>(false);
useEffect(() => {
x.Log ? setPatched(true) : setPatched(false);
return () => {
setIsPatched(false);
};
}, []);
const setPatched = (patched: boolean) => {
setIsPatched(patched);
};
Other component db call:
useEffect(() => {
if (disabled === true) {
const handle = setTimeout(() => setDisabled(false), 7000);
return () => clearTimeout(handle);
}
}, [disabled]);
function handleClick() {
[...]
const updatePatchedX = async (id: string) => {
//check if patched x already in db
const content = await services.xData.getxContent(id);
const xyToUpdated = content?.p[0] as T;
if (!xToUpdated.log) {
// add log property to indicate it is patched and put in DB
xToUpdated.log = [
{ cId: cId ?? "", uId: uId, appliedAt: Date.now() },
];
if (content) {
await services.xData
.updateOTxBook(id, content, uId)
.then(() => {
console.log("done");
setPatched(true);
setDisabled(true);
});
}
}
};
updatePatchedX(notebookID);
}
The UI is only fixed on refresh - not immediately, as the useEffect is supposed to achieve? Not sure where to go from here. Could be a race condition?
I have experienced this in the past and here's what I learned from it
This is normally caused by this sequence of events:
user clicks button => triggers API call => UI changes and the button gets unmounted => API call finishes and tries to update the state of a component that has been unmounted
If the action could be canceled then the default recommendation of "To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function." makes sense; but in this case the API call cannot be cancelled.
The only solution I've found to this is using a ref to track whether the component has been unmounted by the time the API call completes
Below a snippet with just the relevant changes for simplicity
const mainRef = useRef(null);
function handleClick() {
[...]
const updatePatchedX = async (id: string) => {
...
await services.xData
.updateOTxBook(id, content, uId)
.then(() => {
if (mainRef.current) {
console.log("done");
setPatched(true);
setDisabled(true);
}
});
...
};
updatePatchedX(notebookID);
}
return (
<div ref={mainRef}>.... <button onClick={handleClick}>...</button></div>
);
The above works because when the component gets unmounted the myRef references get emptied but you can still check its value when the API call eventually fulfills and before you use some setState function

How to cancel asynchronous tasks in a useEffect cleanup function?

I am getting this error:
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.
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 Products (http://localhost:3000/static/js/main.chunk.js:2779:5)
at div
at Home
at RenderedRoute (http://localhost:3000/static/js/vendors~main.chunk.js:246119:5)
at Routes (http://localhost:3000/static/js/vendors~main.chunk.js:246568:5)
at Router (http://localhost:3000/static/js/vendors~main.chunk.js:246499:15)
at BrowserRouter (http://localhost:3000/static/js/vendors~main.chunk.js:244709:5)
at div
at App
I assume the problem is here:
Products.js
const [products, setProducts] = useState([]);
useEffect(() => {
const getProdcuts = async () => {
try {
const res = await axios.get(
category
? `http://localhost:5000/e-mart/products?category=${category}`
: `http://localhost:5000/e-mart/products`
);
setProducts(res.data);
} catch (err) {
console.log(err.message);
}
};
getProdcuts();
}, [category]);
My home page is not loading. No problem is shown in the terminal. How can I resolve this?
You can use the AbortController to abort an async request, it'd look like
useEffect(() => {
const abortController = new AbortController();
const getProdcuts = async () => {
try {
const res = await axios.get(
category
? `http://localhost:5000/e-mart/products?category=${category}`
: `http://localhost:5000/e-mart/products`,
{ signal: abortController .signal } // Notice this line here
);
setProducts(res.data);
} catch (err) {
console.log(err.message);
}
};
getProdcuts();
return () => {
// Function returned from useEffect is called on unmount
// Here it'll abort the fetch
abortController.abort();
}
}, [category]);
That's a quick way to fix this problem but if you're not sure why this problem is happening in the first place it's probably because you're not expecting Products to unmount.
You might have a parent component rendering Products or one of its parents conditionnally and a state change cause an unmount before fetch is done.

Can't perform a React state update on an unmounted component warning in Reactjs

I'm getting this below warning in console when i run he code.
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.
Here is my code:
const [userDetail, setUserDetail] = React.useState('');
const personalDetails = (user_id) => {
axios
.get(`http://localhost:3001/users/${user_id}`, { withCredentials: true })
.then((response) => {
const personalDetails = response.data;
setUserDetail(response.data);
})
.catch((error) => {
console.log(" error", error);
});
};
React.useEffect(() => {
if (user_id) {
personalDetails(user_id);
}
}, [user_id]);
If I remove useEffect call, this error disappears. What is wrong here?
This seems to be what's happening:
Your component mounts.
Your effect runs, which begins making an HTTP request.
The component unmounts for whatever reason (the parent component is removing it).
The HTTP request completes, and your callback calls setUserDetail, even though the component has been unmounted.
Another issue is that you're defining the personalDetails outside of the effect where it's used.
I would make two changes:
Move the body of the personalDetails function inside the effect.
If the component unmounts, or user_id is changed, then you don't want to call setUserDetail anymore, so use an aborted variable to keep track of this.
const [userDetail, setUserDetail] = React.useState("");
React.useEffect(() => {
if (user_id) {
let aborted = false;
axios
.get(`http://localhost:3001/users/${user_id}`, { withCredentials: true })
.then((response) => {
const personalDetails = response.data;
// Don't change state if the component has unmounted or
// if this effect is outdated
if (!aborted) {
setUserDetail(response.data);
}
})
.catch((error) => {
console.log(" error", error);
});
// The function returned here is called if the component unmounts
// or if the dependency list changes (user_id changes).
return () => (aborted = true);
}
}, [user_id]);

How to stop memory leak in useEffect hook react

I am using Effect hook to fetch the datas from server and these data are passed to the react table there i have used the same api call to load the next set of datas from server.
When the application gets loaded i am getting an warning like below
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.
Effect Hook:
useEffect(() => {
setPageLoading(true);
props
.dispatch(fetchCourses())
.then(() => {
setPageLoading(false);
})
.catch((error: string) => {
toast.error(error);
setPageLoading(false);
});
}, []);
React Table Page:
<ReactTable
className="-striped -highlight"
columns={columns}
data={coursesData}
defaultPage={currentPage}
defaultPageSize={courses.perPage}
loading={isLoading}
manual={true}
onFetchData={setFilter}
/>
Set Filter function:
const setFilter = (pagination: any) => {
props.dispatch(updateCoursePageSize(pagination.pageSize));
props.dispatch(updateCourseCurrentPage(pagination.page + 1));
setCurrentPage(pagination.page);
setPerPage(pagination.pageSize);
setLoading(true);
props.dispatch(fetchCourses()).then(() => {
setLoading(false);
});
};
Does anyone know how to clean up the hook in react
Update (June 2022):
React 18 has removed this warning message, and the workarounds to get rid of it may no longer be necessary. Part of the reason they removed it is that it has always been a bit misleading. It says you have a memory leak, but often times you don't.
The code in the question -- and indeed most code that causes this warning -- runs for a finite amount of time past the unmounting of the component, then sets state, then is done running. Since it's done running, javascript can free up variables in its closure, and thus there is usually no leak.
The case where you will have a memory leak is if you are setting up a persistent subscription which continues indefinitely. For example, maybe you set up a websocket and listen to messages, but you never tear down that websocket. These cases do need to be fixed (by supplying a cleanup function to the useEffect) but they are uncommon.
The other reason react 18 has removed the warning is that they are working on the ability for components to preserve their state after being unmounted. Once that feature is in react, setting state after unmount will be a perfectly valid thing to do.
Original answer (September 2019):
With useEffect you can return a function that will be run on cleanup. So in your case, you'll want something like this:
useEffect(() => {
let unmounted = false;
setPageLoading(true);
props
.dispatch(fetchCourses())
.then(() => {
if (!unmounted) {
setPageLoading(false);
}
})
.catch((error: string) => {
if (!unmounted) {
toast.error(error);
setPageLoading(false);
}
});
return () => { unmounted = true };
}, []);
EDIT: if you need to have a call that's kicked off outside of useEffect, then it will still need to check an unmounted variable to tell whether it should skip the call to setState. That unmounted variable will be set by a useEffect, but now you need to go through some hurdles to make the variable accessible outside of the effect.
const Example = (props) => {
const unmounted = useRef(false);
useEffect(() => {
return () => { unmounted.current = true }
}, []);
const setFilter = () => {
// ...
props.dispatch(fetchCourses()).then(() => {
if (!unmounted.current) {
setLoading(false);
}
})
}
// ...
return (
<ReactTable onFetchData={setFilter} /* other props omitted */ />
);
}
you can create a custom hook for that like that :
import * as React from 'react';
export default function useStateWhenMounted<T>(initialValue: T) {
const [state, setState] = React.useState(initialValue);
const isMounted = React.useRef(true);
React.useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
const setNewState = React.useCallback((value) => {
if (isMounted.current) {
setState(value);
}
}, []);
return [state, setNewState];
}
Memory leak happens, when a thing that is unnecessary and is supposed to be cleared from memory is kept because some other thing is still holding it. In React Component case, the async call made in component may hold the references of setState or other references and will hold them until the call completes.
The warning you see is from React saying that something is still holding and setting state of a component instance that was removed from tree long back when component unmounted. Now using a flag to not set the state only removes the warning but not the memory leak, even using Abort controller does the same. To escape this situation you can use state management tools that helps dispatching an action which will do processing out side of component without holding any memory references of the component, for example redux. If you are not using such tools then you should find a way to clear the callbacks you pass to the async call (then, catch, finally blocks) when component unmounts. In the below snippet I am doing the same detaching the references to the methods passed to async call to avoid memory leaks.
Event Emitter here is an Observer, you can create one or use some package.
const PromiseObserver = new EventEmitter();
class AsyncAbort {
constructor() {
this.id = `async_${getRandomString(10)}`;
this.asyncFun = null;
this.asyncFunParams = [];
this.thenBlock = null;
this.catchBlock = null;
this.finallyBlock = null;
}
addCall(asyncFun, params) {
this.asyncFun = asyncFun;
this.asyncFunParams = params;
return this;
}
addThen(callback) {
this.thenBlock = callback;
return this;
}
addCatch(callback) {
this.catchBlock = callback;
return this;
}
addFinally(callback) {
this.finallyBlock = callback;
return this;
}
call() {
const callback = ({ type, value }) => {
switch (type) {
case "then":
if (this.thenBlock) this.thenBlock(value);
break;
case "catch":
if (this.catchBlock) this.catchBlock(value);
break;
case "finally":
if (this.finallyBlock) this.finallyBlock(value);
break;
default:
}
};
PromiseObserver.addListener(this.id, callback);
const cancel = () => {
PromiseObserver.removeAllListeners(this.id);
};
this.asyncFun(...this.asyncFunParams)
.then((resp) => {
PromiseObserver.emit(this.id, { type: "then", value: resp });
})
.catch((error) => {
PromiseObserver.emit(this.id, { type: "catch", value: error });
})
.finally(() => {
PromiseObserver.emit(this.id, { type: "finally" });
PromiseObserver.removeAllListeners(this.id);
});
return cancel;
}
}
in the useEffect hook you can do
React.useEffect(() => {
const abort = new AsyncAbort()
.addCall(simulateSlowNetworkRequest, [])
.addThen((resp) => {
setText("done!");
})
.addCatch((error) => {
console.log(error);
})
.call();
return () => {
abort();
};
}, [setText]);
I forked someones code from here to use above logic, you can check it in action in the below link
link
The other answers work of course, I just wanted to share a solution I came up with.
I built this hook that works just like React's useState, but will only setState if the component is mounted. I find it more elegant because you don't have to mess arround with an isMounted variable in your component !
Installation :
npm install use-state-if-mounted
Usage :
const [count, setCount] = useStateIfMounted(0);
You can find more advanced documentation on the npm page of the hook.

Authentication listeners when refactoring to React hooks

I'm having a little trouble figuring out how to change my authentication handling component when refactoring from a React class to React hooks.
Here's the relavant code in my class:
state = {
user: null
}
componentDidMount() {
authGetUser(user => {
if (user !== this.state.user) {
this.setState({user})
}
})
}
componentWillUnmount() {
authUnsubscribe()
}
handleAuthClick = () => {
if (this.state.user) {
authSignOut()
} else {
authSignIn()
}
}
And here it is with hooks:
const [user, setUser] = useState<firebase.User | null>(null)
useEffect(() => {
return authUnsubscribe() // runs on mount and unmount only
}, [])
useEffect(() => {
authGetUser(usr => setUser(usr))
}, [])
const handleAuthClick = () => {
if (user) {
authSignOut()
} else {
authSignIn()
}
}
Also, here are my other relevant methods:
const authGetUser = (callback: (user: firebase.User | null) => void) => {
initFirebase()
authUnsubscribe()
userUnsubscribe = firebaseAuth.onAuthStateChanged(callback)
}
export const authUnsubscribe = () => {
if (userUnsubscribe) {
userUnsubscribe()
}
}
const authSignIn = () => {
googleAuth.signIn().then((googleUser: any) => {
var credential = firebase.auth.GoogleAuthProvider.credential(googleUser.getAuthResponse().id_token)
firebaseAuth.signInAndRetrieveDataWithCredential(credential)
})
}
const authSignOut = () => {
googleAuth
.signOut()
.then(firebaseAuth.signOut())
}
Both examples work. However, when I log out and log in with the hooks version, I get an error message in console saying
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 the componentWillUnmount method.
which suggests that the cleanup is not done properly.
Yes, I know I could just continue using the version with the class which works. But I want to understand React hooks better by solving this.
Any ideas?
Wouldn't this works for you? You could use a single useEffect().
React Hooks API DOCs
useEffect(
() => {
const subscription = props.source.subscribe();
return () => {
// Clean up the subscription
setUser(null); // <--- TRY DOING SOMETHING LIKE THIS
subscription.unsubscribe();
};
},
[],
);
The clean-up function runs before the component is removed from the UI
to prevent memory leaks. Additionally, if a component renders multiple
times (as they typically do), the previous effect is cleaned up before
executing the next effect. In our example, this means a new
subscription is created on every update. To avoid firing an effect on
every update, refer to the next section.
If you want to run an effect and clean it up only once (on mount and
unmount), you can pass an empty array ([]) as a second argument. This
tells React that your effect doesn’t depend on any values from props
or state, so it never needs to re-run. This isn’t handled as a special
case — it follows directly from how the dependencies array always
works.
Generally, this happens when we have asynchronous requests and the component is unmounted before, occurring memory leak. Obviously, that this not occur in class-based components because we have componentDidMount() and componentWillUnmount() hooks, so it's more confident than useEffect() that we have manipulated the state, so I think that you need to identify the reason for the application unmount and there is the solution.
You should use one useEffect() instead two like this:
useEffect(() => {
authGetUser(usr => setUser(usr))
return authUnsubscribe() // runs on mount and unmount only
}, [])

Resources