JoshWComeau

useTimeout

Filed under
Snippets
on
in
July 29th, 2021.
Jul 2021.
import React from 'react';

export default function useTimeout(callback, delay) {
  const timeoutRef = React.useRef(null);
  const savedCallback = React.useRef(callback);

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

  React.useEffect(() => {
    const tick = () => savedCallback.current();

    if (typeof delay === 'number') {
      timeoutRef.current = window.setTimeout(tick, delay);

      return () => window.clearTimeout(timeoutRef.current);
    }
  }, [delay]);

  return timeoutRef;
};

Link to this headingContext

JavaScript provides a handy method for executing some code after a specified amount of time: window.setTimeout.

When working with React, however, we can run into some problems if we try to use it as-is.

This hook is a "react-friendly" wrapper around setTimeout. You can use it just like you'd use window.setTimeout, and it'll work as you expect.

Link to this headingUsage

function App() {
  const [hasTimeElapsed, setHasTimeElapsed] = React.useState(false);

  useTimeout(() => {
    setHasTimeElapsed(true);
  }, 5000);

  return (
    <p>
      {hasTimeElapsed
        ? '5 seconds has passed!'
        : 'The timer is running…'}
    </p>
  )
}

In this example, the first render will happen instantly, with hasTimeElapsed being false. Then, 5 seconds later, it'll re-render with hasTimeElapsed set to true.

Any other renders in the meantime, caused by a parent component, won't affect anything.

Link to this headingCancelling

You can cancel the timeout by changing the delay property to null:

function App() {
  const [abortTimeout, setAbortTimeout] = React.useState(false);
  const [hasTimeElapsed, setHasTimeElapsed] = React.useState(false);

  useTimeout(() => {
    setHasTimeElapsed(true);
  }, abortTimeout ? null : 5000);

  return (
    <p>
      {hasTimeElapsed && 'Boom!'}
      <button onClick={() => setAbortTimeout(true)}>
        Diffuse Bomb
      </button>
    </p>
  )
}

In this example, if the user clicks the button before the timeout has expired, the timeout will be canceled, and will never fire.

You can also capture the timeout ID to cancel it imperatively:

function App() {
  const [hasTimeElapsed, setHasTimeElapsed] = React.useState(false);

  const timeoutRef = useTimeout(() => {
    setHasTimeElapsed(true);
  }, 5000);

  return (
    <p>
      {hasTimeElapsed && 'Boom!'}
      <button onClick={() => window.clearTimeout(timeoutRef.current)}>
        Diffuse Bomb
      </button>
    </p>
  )
}

Link to this headingExplanation

You might be wondering: why is this needed? Can't you just use setTimeout instead?

There are 3 problems with using window.setTimeout in React:

  1. This will break if your application is statically-generated or server-side rendered, since window isn't definedWe could remove the `window` and run `setTimeout()`, which does exist in a Node context, but that might cause lots of funky issues / memory leaks.
  2. A new timeout will be scheduled whenever this component renders, instead of only once when the component mounts. If our parent component re-renders 10 times, we'll schedule 10 timeouts.
  3. Our callback function is stale; it won't have access to current values for state and props.

That last issue is tricky, and it requires a very clear mental model of how React and JavaScript work (and how they're a bit incompatible with each other).

Dan Abramov wrote about this discrepancy, and how to overcome it, in a fantastic article about useInterval(opens in new tab). The exact same principles apply for useTimeout. If you're keen to develop a deeper understanding of React, I highly recommend checking it out.

Last updated on

July 29th, 2021

# of hits