Why you shouldn't save the interval id in the state
When working with intervals, it's important to also clear them.
You can achieve this by passing the interval id you received when starting the interval to the clearInterval
function:
// start an interval and get its id
const intervalId = setInterval(() => console.log('hello'), 1000);
// clear the interval
clearInterval(intervalId);
When using intervals inside a React component, you'd want to save a reference to the interval id so you can later clear it. And it's tempting to just pop it into a state variable:
// store interval id in state
const [intervalId, setIntervalId] = useState<number | null>(null);
const handleClickStart = () => {
const intervalId = window.setInterval(() => {
// ...
}, 1000);
setIntervalId(intervalId);
};
const handleClickStop = () => {
// If the timer is already stopped, do nothing
if (intervalId === null) {
return;
}
clearInterval(intervalId);
setIntervalId(null);
};
But there's a problem with this approach - every time you would call setIntervalId
, you would trigger an unnecesarry rerender. Also, when you would clear the interval on unmount, you'd have access to the "old" value of the interval id, due to a "stale closure":
useEffect(() => {
return () => {
clearInterval(intervalId)
};
}, [intervalId]);
A better approach is using a ref
to store the interval id. This way, we can still keep a reference to the id of the currently running timer, but we no longer cause unnecessary renders and we have access to the latest variable when clearing the interval on unmount.
// store interval id as ref
const intervalId = useRef<number | null>(null);
const handleClickStart = () => {
// update current value of the ref
// does not trigger a rerender
intervalId.current = window.setInterval(() => {
// ...
}, 1000);
};
const handleClickStop = () => {
// If the timer is already stopped, do nothing
if (intervalId.current === null) {
return;
}
clearInterval(intervalId.current);
// clear current value of the ref
// does not trigger a rerender
intervalId.current = null;
};
// Cleanup the timer on unmount
useEffect(() => {
return () => {
clearInterval(intervalId.current)
};
}, []);
Check out these challenges:
- Create a timer that can be started and stopped
- Build a Typewriter effect component
Member discussion