user3343396
user3343396

Reputation: 765

Can't perform a React state update on an unmounted component - even when clearing interval

I'm facing a strange issue.

I have a Message component that contains the following code:

const Message = (props) => {

    const [timeout, setTimerTimeout] = useState(null);
    const [someVar, setSomeVar] = useState(null);

    useEffect(() => {
        setTimerTimeout(prevTimeout => {
            if (prevTimeout) {
                clearInterval(prevTimeout);
            }
            return setInterval(someFunc, 1000)
        });
    }, [someVar]);

    useEffect(() => {
        return () => {
            clearInterval(timeout);
        };
    }, []);

    ...
}

Even though I'm clearing the interval in the return func of useEffect, I'm getting this message in the console (pointing to Message component):

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.

When returning null instead of setInterval(someFunc, 1000), the warning is gone (but of course this is not what I want, i just know that the interval is causing the problem).

I don't know what I am doing wrong and how to get rid of it.

Any idea? Thanks!

Upvotes: 0

Views: 180

Answers (3)

user12697177
user12697177

Reputation:

You can use a guard variable which cancels the async action (in this case a timer, or in another scenario a fetch request) if the component is unmounted prior to it's return.

const Message = (props) => {

  const [startInterval, setStartInterval] = useState(null);  

  useEffect(() => {

    let cancel = false;

    const intervalID = setInterval(() => { // run every second
      if (!cancel) {                       // but don't run if component unmounts
        someFunc();
      }
    }, 1000);

    return () => {               // runs when component unmounts
      cancel = true;             // stop an already-running interval
      clearInterval(intervalID); // stop the interval generator
    });

  }, [startInterval]);

  ...

}

If you use setSomeVar to allow the user to manually stop the timer, e.g on a click event

const Message = (props) => {

  const [startInterval, setStartInterval] = useState(null);  
  const [stopInterval, setStopInterval] = useState(null);  

  useEffect(() => {

    let cancel = false;

    let intervalID;
    if (stopInterval && intervalID) {

      clearInterval(intervalID); // manually stop the interval generator

    } else {

      intervalID = setInterval(() => { // run every second
        if (!cancel) {                 // but don't run if component unmounts
          someFunc();
        }
      }, 1000);
    }

    return () => {               // runs when component unmounts
      cancel = true;             // stop an already-running interval
      clearInterval(intervalID); // stop the interval generator
    });

  }, [startInterval, stopInterval]);

  ...

}

Upvotes: 0

mindlis
mindlis

Reputation: 1677

The cleanup function for your useEffect hook should be part of the same call that sets up the effect.

Try this:

useEffect(() => {
    () => setTimerTimeout(prevTimeout => {
        if (prevTimeout) {
            clearInterval(prevTimeout);
        }
        return setInterval(someFunc, 1000)
    });
    return () => clearInterval(timeout);
}, [someVar]);

https://reactjs.org/docs/hooks-effect.html#effects-with-cleanup

Upvotes: 0

Nicholas Tower
Nicholas Tower

Reputation: 85012

Your second useEffect is only created once, when the component renders for the first time, and so the value it has for timeout is null. So it will clear null.

You don't need two effects though, you just need one. You can modify your first effect to include a teardown function, and to not need to save the timer id to state:

useEffect(() => {
  let id = setInterval(someFunc, 1000);
  return () => clearInterval(id);
}, [someVar]);

Upvotes: 2

Related Questions