Did you ever got the following error?
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.
The message is straightforward. We're trying to change the state of a component, even after it's been unmounted and unavailable.
There are multiple reasons why this can happen but the most common are that we didn’t unsubscribe to a websocket component, or this were dismount before an async operation finished.
How can we fix this?
Cleanup function in the useEffect hook.
The useEffect hook is built in a way that if we return a function within the method, this function will execute when the component gets disassociated. This is very useful because we can use it to remove unnecessary behavior or prevent memory leaking issues.
So, if we want to cleanup a subscription, the code would look like this:
useEffect(() => {
API.subscribe()
return function cleanup() {
API.unsubscribe()
}
})
Don't update the state on an unmounted component
One common implementation is to update the component state once an async function finishes. But what happen if the component unmounts after finishing? It will try to set the state anyway if we not control that.
In a real scenario, it happened to me on React Native that an user can leave a screen before a process ends.
In the following example, we have an async function that performs some operation and while this is running I want render a “loading” message. Once the function finish I will change the state of “loading” and render another message.
function Example(props) {
const [loading, setloading] = useState(true)
useEffect(() => {
fetchAPI.then(() => {
setloading(false)
})
}, [])
return <div>{loading ? <p>loading...</p> : <p>Fetched!!</p>}</div>
}
But, if we exit the component and fetchAPI ends and sets the loading state, this will prompt the error mentioned at the beginning. So we need to be sure that the component is still mounted when fetchAPI is finished.
function Example(props) {
const [loading, setloading] = useState(true)
useEffect(() => {
let mounted = true
fetchAPI.then(() => {
if (mounted) {
setloading(false)
}
})
return function cleanup() {
mounted = false
}
}, [])
return <div>{loading ? <p>loading...</p> : <p>Fetched!!</p>}</div>
}
This way we can ask if the component is still mounted. Just adding a variable that will change to false if we dismount.
Extra: Cancel an Axios request
Axios comes with a cancellation option to finish a request before it ends. This is useful besides the cleanup function to prevent memory leaking.
useEffect(() => {
const source = axios.CancelToken.source()
const fetchUsers = async () => {
try {
await Axios.get('/users', {
cancelToken: source.token,
})
// ...
} catch (error) {
if (Axios.isCancel(error)) {
} else {
throw error
}
}
}
fetchData()
return () => {
source.cancel()
}
}, [])
Conclusion
There's a lot of other usages of the cleanup function on the useEffect hook, but I hope this can give you a better perspective of how and when to use it.
Please add any comment or suggestion, I will appreciate.
Latest comments (32)
prefect
I use useEffect to call a function which calls the api via axios only on the initial render. I don’t get any errors, do I still need to unsubscribe and do all this?
Thank you!! Thanks to this article I finally understand what this is about and what the use case could be. Saludos!
Noice
Thanks, nice article.
What about putting setloading(true) in the return function?
ex.
return function cleanup() {
setloading(true);
}
Would it differ from mounted = false? and why?
Thank you in advance.
In the last eample (Extra: Cancel an Axios request)
There is a typo: Before the cleanup function, it should be "fetchUsers()" instead "fetchData()"
Good article, tho! Thanks!
hii here i'm not performing any async operation event though i'm still getting warnings with cleanup function when i'm routing to another page. Please can you suggest me any solution.
export default function useOnScreen(ref, rootMargin = "0px") {
const [isVisible, setIsVisible] = useState(false)
useEffect(() => {
let setRef=null;
if (ref.current === null) return
const observer = new IntersectionObserver(
([entry]) => setIsVisible(entry.isIntersecting),
{ rootMargin }
)
observer.observe(ref.current)
setRef=ref
return () => {
if (setRef.current) {
observer.unobserve(setRef.current)
}
}
}, [ref, rootMargin])
return isVisible
}
Thanks Man! This is quite helpful.
Many thanks, this was really helpful! I got this error "Warning: An update to SomeComponent inside a test was not wrapped in act(...)." when running my tests. Took me a while to figure out this was coming from a fetch() call in a useEffect() method that was updating some state. Applied the "only update state when mounted" fix you suggested and that solved it.
In the last axios example, shouldn't the method call be fetchUsers() and not fetchData()?
yes. it should be fetchUsers()