React cleaner use of setTimeout

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

I'm a full-stack developer from South Africa 🇿🇦. I love writing about JavaScript, HTML and CSS.
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.
Most of you know me for my consistency, a golden arrow in my blog series. I've written 1000 articles in 1008 days! Almost an article a day, and my honeymoon was the only holiday I ever took. I'm super proud of this achievement; it has been a fantasti...

It's not the first time I'll be talking about community. I think it's an essential aspect of any successful tool. This shows in my previous explorations of Astro, Medusa, and now Vendure as well. All these products thrive in a super open, welcoming, ...

The cool part about Vendure is how easy it is to set up and how abstract each layer is. Basically, we get the following elements: External database Server Worker Admin UI Frontend While this is amazing, it also brings a bit of complexity when it co...

The previous article looked at customizing Vendure on a data and process level. In this article, we'll look at customizing emails, as they are often a big part of a webshop system. We'll be looking at two different layers of customization for customi...

Even though Vendure is a pretty significant project out of the box, in some cases, we might want to go in and modify some elements to work to our specific use case. In this article, I'll take a high-level look at some elements we can customize within...

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.
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?
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.
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.
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 my blog. Feel free to subscribe to my email newsletter and connect on Facebook or Twitter