How to use clearInterval() inside React's useEffect(), and why it is important.
How to use clearInterval() inside React's useEffect(), and why it is important
Using setInterval
with useEffect
can be confusing. Often they will overlap, or use stale data. We can prevent this by properly clearing the intervals inside useEffect
's cleanup function.
What happens if we don't clear the intervals?
Suppose we have a component which shows blinking text, like we used to have in html.
const Blinker = ({ text }) => {
const [visible, setVisible] = useState(true);
useEffect(() => {
setInterval(() => {
console.log(`Current blinking text: ${text}`);
setVisible((visible) => !visible);
}, 1000);
}, [text]);
return visible ? <h1>{text}</h1> : null;
};
Note: The [text]
dependency of useEffect is causing the hook to re-execute every time the prop text changes. The only reason to add this dependency is to make the console.log
work properly.
Since we could consider it to be non-vital to our app, we could remove it. But considering there are lots of valid reasons to add a prop to the dependencies, let's assume the console.log
is a strong requirement.
What happens with Blinker when we render for the first time (i.e on mounting)
- The effect is ran for the first time, since effects always run on the first render (mounting).
- The first interval is started, which logs an
Current blinking text:
string every second. - The component returns an empty header, which the browser renders.
What happens with Blinker when we change the text prop to "a"
- The Blinker component renders again, since whenever props or state change, react will re-render our component.
- React checks the useEffect's dependencies, and since one changed (text), it executes the effect's function again.
- A new interval is registered, which will print
Current blinking text: a
every second. - The component returns a header with the letter "a", which also shows up on the screen.
In this scenario, the browser's console looks like this:
2 intervals are running at the same time, each logging a different thing. This happens because we didn't delete the old interval before creating a new one, so the old one never stopped logging!
Solution
To solve this, we can use useEffect's cleanup function, which looks like this:
const Blinker = ({ text }) => {
const [visible, setVisible] = useState(true);
useEffect(() => {
const intervalId = setInterval(() => {
console.log(`Current blinking text: ${text}`);
setVisible((visible) => !visible);
}, 1000);
return () => {
clearInterval(intervalId);
};
}, [text]);
return visible ? <h1>{text}</h1> : null;
};
What happens now is:
- The Blinker component renders again, same as before, since props changed.
- React checks useEffect's dependencies, and since they changed, it executes the effect's function again.
- But first, before react executes the effect, it will run the function we returned, cleaning up previous effect and deleting the old interval.
- A new interval is registered, which will print
The text currently blinking is: a
every second. - The component returns a header with the letter "a", which also shows up on the screen.
This prevents overlapping of the intervals, and makes our code behave the way we wanted. Keep in mind react will run this cleanup function before re-running an effect, and when unmounting the component. So basically we cleanup before reacting to changes, and when we don't need the component anymore.
You can see both examples above working in this codesandbox, remember to switch the exported component in Blinker.js to see both behaviors shown in this article.
There are many other scenarios where useEffect's cleanup function is useful or neccessary, stay tuned for more examples in the future!
Thank you, Damian, for this informative blog!
Savior !!! was stuck on it for a while a big thanks to you !
Great explanation! Thank you Damian!