How (and why) to use useEffect's cleanup - Intervals
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.
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.
Si querés leer este post en español, clickeá acá.
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 run for the first time, since effects always run on the first render (mounting).
- The first interval is started, which logs a 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:
Two 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 the previous effect’s function 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 necessary, stay tuned for more examples in the future!
_________________________________________________________