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.
It’s pretty common to get the above error in your code if you try to set a state when a component is no longer mounted. And while it’s best to write your components to avoid this situation, sometimes it’s unavoidable.
Before we look at how you can replicate the deprecated isMounted
function with hooks, let me start by saying, you shouldn’t need to!
isMounted
was deprecated for a reason. It hides the error but doesn’t solve the problem. Your redundant code still runs, you just prevent it from setting the state.
If the response triggers a state change then you're going to get that warning message.
A better option is to clean up your code by returning a cleanup function from useEffect
. I covered this in more detail in how to use React useEffect
like componentDidMount
.
But there may be some cases where you can't clean up your code immediately, for example, maybe a debounce function might still run once after a dismount.
Ok, so if you understand why isMounted
was depreciated, but still need it, how can you do that with hooks?
Starting with useEffect
As I already covered in how to use React useEffect
like componentDidMount
, useEffect
is the ReactJS hooks way of handling lifecycle changes in your components.
So when you need to know if a component is mounted or not, useEffect
is a good place to start.
useEffect(() => {
// component is mounted
return () => {
// component is being unmounted
}
});
But the above code will run on every render of your component, e.g. when state or props change. That's not what we want here. You just need to useEffect
to run on the initial mount, and the finale unmount.
So we can add an empty argument to tell useEffect
to run once.
useEffect(() => {
// component is mounted
return () => {
// component is being unmounted
}
}, []); // run once on mount
Adding useRef
to store isMounted
Ok, now we need to set some kind of persistent value that will stick around between renders so we know that our component is mounted.
Usually, when you want to keep a value around in a React component, you would reach for state and the useState
hook. But we will avoid using state for this because changing the state will trigger a re-render.
A re-render might trigger other code that sets state and we can't set state when a component unmounts, that's kinda the whole problem.
Instead, you can useRef
. Because it doesn't do any of that!
You'll useEffect
to monitor when the component mounts or unmounts, and save that info in a ref
.
const isMounted = useRef(false); // unmounted by default
useEffect(() => {
isMounted.current = true; // mounted
return () => {
isMounted.current = false; // unmounted
}
}, []); // run once on mount
Now anytime you need to know if our component is mounted, you can check isMounted.current
to get the current value.
// in an if block
if (isMounted.current) {
// can set state because component is mounted
}
// shorthand
isMounted.current && setState("Yeah we can do this!");
Adding useCallback
to create a custom hook
Let's extract this code into a custom hook we can reuse in any component.
If you want to create a custom hook, you can return the ref, and check the current value like in the code example above.
But there are a couple of improvements you can make.
First of all, we don't want to add .current
every time. It doesn't look pretty, and we shouldn't need to remember the hook returns a ref
to use it.
You might be tempted to just return isMounted.current
from a custom hook (so it just returns true
or false
. But you would need to pass it as a dependency in any other useEffect
you used it in or the value wouldn't update.
Instead, you can simplify things by returning a function that checks isMounted.current
.
Make sure to wrap it in a useCallback
function though. This will memoize the function so that its value (the function not the ref) doesn't change, and it will avoid re-renders or needing to be a dependency.
Here's how the hook will look.
import { useCallback, useEffect, useRef } from 'react'
function useIsMounted() {
const isMounted = useRef(false); // unmounted by default
const isMountedFunction = useCallback(() => isMounted.current, []);
useEffect(() => {
isMounted.current = true; // mounted
return () => {
isMounted.current = false; // unmounted
}
}, []); // run once on mount
return isMountedFunction;
};
Or you can refactor that slightly to return the useCallback
function like this:
import { useCallback, useEffect, useRef } from 'react'
function useIsMounted() {
const isMounted = useRef(false); // unmounted by default
useEffect(() => {
isMounted.current = true; // mounted
return () => {
isMounted.current = false; // unmounted
}
}, []); // run once on mount
return useCallback(() => isMounted.current, []); // return function that checks mounted status
};
export default useIsMounted;
Using the isMounted
custom hook
Now you can use the hook in any component, to check if the component is mounted or not before doing things like setting state.
import React from 'react';
import useIsMounted from './hooks/useIsMounted';
function MyComponent() {
const [state, setState] = useState('');
const isMounted = useIsMounted();
// in an if block
if (isMounted()) {
// can set state because component is mounted
}
// shorthand
isMounted() && setState("Yeah we can do this!");
//...
};