Skip to main content

Command Palette

Search for a command to run...

React cleaner use of setTimeout

Published
3 min read
React cleaner use of setTimeout
C

I'm a full-stack developer from South Africa 🇿🇦. I love writing about JavaScript, HTML and CSS.

When working with setTimeout we generally don't have to worry about cleaning up our timeouts.

However, introducing it into React can create some nasty edge-cases.

This often happens because we want to manipulate data after x time. The component might be unmounted by then, but the timeout is still trying to activate.

You might see some edge cases where your interactions seem to be reverted. Or even get memory leak messages in your console.

Clear your timeouts!

The general rule of advice is to keep track of the timeouts you create in your code and clean them.

To clean your timeouts, we can leverage the useEffect cleanup function.

A quick example could look like this:

export default function Test() {
  const [show, setShow] = useState(false);
  useEffect(() => {
    const test = window.setTimeout(() => {
      setShow(false);
    }, 1500);
    return () => {
      clearInterval(test);
    };
  }, []);

  return (
    <div>
      <h1>Loading...</h1>
      {show && <p>I'm fully loaded now</p>}
    </div>
  );
}

However, I prefer to use a reference to clear the interval.

const timeoutRef = useRef();

useEffect(() => {
  timeoutRef.current = window.setTimeout(() => {
    setShow(false);
  }, 1500);
  return () => clearInterval(timeoutRef.current);
}, []);

This will work, but it's a bit of a hassle to remember to clean this up on unmount, etc.

So why not create a small hook for it?

React useTimeout hook

We can start by introducing a useTimeout hook. This hook will be our React version of the setTimeout function.

This hook should have the following options.

  • Receive the callback function (an action that should happen after the timeout)
  • Receive the delay (time for it to timeout)
  • Return a function that can be invoked to start it
import { useCallback, useEffect, useRef, useMemo } from 'react';

export default function useTimeout(callback, delay) {
  const timeoutRef = useRef();
  const callbackRef = useRef(callback);

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

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

  const memoizedCallback = useCallback(
    (args) => {
      if (timeoutRef.current) {
        window.clearTimeout(timeoutRef.current);
      }
      timeoutRef.current = window.setTimeout(() => {
        timeoutRef.current = null;
        callbackRef.current?.(args);
      }, delay);
    },
    [delay, timeoutRef, callbackRef]
  );

  return useMemo(() => [memoizedCallback], [memoizedCallback]);
}

First, we see the parameters passed as callback and delay. Then we add two references to keep track of the active timeout and active callback.

Then we'll need two useEffects, the first one to listen to the callback in case it changes after rendering (this could happen if you change any state inside the callback).

The second one is used to handle the cleanup effect for the timeout. (When the component gets unmounted)

Then we create a useCallback, where we first clear out any existing timeouts in our ref. Then we assign the new timeout. This whole callback listens to changes on all our variables.

And the last part is to return a memoized function that will listen to changes on its callback.

This might seem an overkill method, but it will help solidify your timeouts and keep everything as clean as possible.

Using the hook

To use the hook, we can introduce the following code.

import useTimeout from './useTimeout';

const [timeout] = useTimeout(() => {
  setShow(false);
}, 1500);

timeout();

Wow, way cleaner, right? And now, we only have one place to keep track of our timeouts and ensure they are constantly cleaned up.

Thank you for reading, and let's connect!

Thank you for reading my blog. Feel free to subscribe to my email newsletter and connect on Facebook or Twitter

M
mamged3y ago

good job, but I think this will cause an error with SSR because window object is not accessible on the server side code

1
C

Very good point yes, in most cases you would already have something in place to handle window defined for the SSR. Forgot to include that in the main article, so thanks for the heads-up.

More from this blog

D

Daily Dev Tips

887 posts

Looking to get into development? As a full-stack developer I guide you on this journey and give you bite sized tips every single day 👊

React cleaner use of setTimeout