1 min read

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)
    };
  }, []);
Want to check your understanding of working with intervals in React?
Check out these challenges:
- Create a timer that can be started and stopped
- Build a Typewriter effect component