Reputation: 765
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
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
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
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