useInterval

useInterval
const useInterval = (callback, delay) => {
  const intervalId = React.useRef(null);
  const savedCallback = React.useRef(callback);

  React.useEffect(() => {
    savedCallback.current = callback;
  });

  React.useEffect(() => {
    const tick = () => savedCallback.current();
    if (typeof delay === 'number') {
      intervalId.current = window.setInterval(tick, delay);
      return () => window.clearInterval(intervalId.current);
    }
  }, [delay]);
  
  return intervalId.current;
}

Why?

Intervals can be pretty tricky in React; if you're not careful, you'll wind up with a "stale" interval, one which can't access current values of state or props.

This hook allows us to sidestep that confusion, and it also gives us a superpower: we can modify the delay without having to worry about stopping and starting the interval.

Usage

1

Basic Usage

useInterval(() => {
  console.log('I fire every second!');
}, 1000);
2

More Advanced Usage

Here's an example of a "Stopwatch" component. useInterval lets us track how much time has elapsed since the user has started the stopwatch.

const Stopwatch = () => {
  const [status, setStatus] = React.useState('idle');
  const [timeElapsed, setTimeElapsed] = React.useState(0);

  useInterval(
    () => {
      setTimeElapsed((timeElapsed) => timeElapsed + 1);
    },
    status === 'running' ? 1000 : null
  );
  
  const toggle = () => {
    setTimeElapsed(0);
    setStatus((status) => (status === 'running' ? 'idle' : 'running'));
  };
  
  return (
    <>
      Time Elapsed: {timeElapsed} second(s)
      <button onClick={toggle}>
        {status === 'running' ? 'Stop' : 'Start'}
      </button>
    </>
  );
};

Let's expand on that with useRandomInterval, which is like setInterval, but more random.

const random = (min, max) => Math.floor(Math.random() * (max - min)) + min;

const useRandomInterval = (callback, minDelay, maxDelay) => {
  const timeoutId = React.useRef(null);
  const savedCallback = React.useRef(callback);
  
  React.useEffect(() => {
    savedCallback.current = callback;
  });

  React.useEffect(() => {
    let isEnabled =
      typeof minDelay === 'number' && typeof maxDelay === 'number';
    if (isEnabled) {
      const handleTick = () => {
        const nextTickAt = random(minDelay, maxDelay);
        timeoutId.current = window.setTimeout(() => {
          savedCallback.current();
          handleTick();
        }, nextTickAt);
      };
      handleTick();
    }
    return () => window.clearTimeout(timeoutId.current);
  }, [minDelay, maxDelay]);
  const cancel = React.useCallback(function () {
    window.clearTimeout(timeoutId.current);
  }, []);
  return cancel;
};

This example uses the hook to create a "laggy" clock (a clock that only updates once every few seconds):

const LaggyClock = () => {
  // Update between every 1 and 4 seconds
  const delay = [1000, 4000];
  const [currentTime, setCurrentTime] = React.useState(Date.now);
  
  useRandomInterval(() => setCurrentTime(Date.now()), ...delay);

  return <>It is currently {new Date(currentTime).toString()}.</>;
}