Reputation: 23
When setState is called after unmounting the Component in an asynchronous clickHandler causes warning in console:
Warning: Can't perform a React state update on an unmounted component.
function App() {
const [show, setShow] = useState(true)
return {show && <Button
text="Click to remove"
clickHandler={() => setShow(false)}
/> }
}
function Button({ text, clickHandler }) {
const [state, setState] = useState(text);
const handleClick = async () => {
await clickHandler();
setState("I was clicked");
};
return <button onClick={handleClick}> {state}</button>;
}
Clicking the click to remove
button will unmount the button and then update the state.
The goal is to set the state of the Button Component after the asynchronous call. This asynchronous call can but does not have to trigger the Button being unmounted. I am looking for a way to opt out of the setState
after the asynchronous call.
I do not see how this can be avoided in a nice way using useEffect.
There are two possible workarounds in the example.
One uses useRef
to check if the component is unmounted (like recommended in #14369 (comment) ). This does not feel very react-like.
The other workaround uses the recommended useEffect
guard variable (like #14369 (comment)). But to get the clickHandler out of useEffect
the clickHandler is stored in a state. However, to get a function into a state it needs to be wrapped inside another function, as the setState
function of useState
will call a function given as an argument.
Upvotes: 2
Views: 1883
Reputation: 4056
This is a great question, I have no idea why it didn't get any attention. I believe I found a nice solution, let me know if you think this solves the problem. The solution is based on the fact that we can use useRef()
without using the ref attribute anywhere.
We define two custom hooks, useIsMountedRef
and useStateWithMountCheck
. You can use the latter just like the useState
hook and it will just ignore any demanded state change if the component is not mounted anymore.
function useIsMountedRef(){
const ref = useRef(null);
ref.current = true;
useEffect(() => {
return () => {
ref.current = false;
}
});
return ref;
}
function useStateWithMountCheck(...args){
const isMountedRef = useIsMountedRef();
const [state, originalSetState] = useState(...args);
const setState = (...args) => {
if (isMountedRef.current) {
originalSetState(...args);
}
}
return [state, setState];
}
Upvotes: 3