palortoff
palortoff

Reputation: 23

Set state on unmounted component outside of useEffect

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.

See Codesandbox Example

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

Answers (1)

Vincent
Vincent

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 useStatehook 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];
}

Checkout the Sandbox.

Upvotes: 3

Related Questions