Home

Blog

setTimeout and clearTimeout in React with hooks

Avoiding memory leaks when components unmount

byAlex Fenwood Hughes

March 05, 2020

Do a quick search and you’ll find many people saying that with setTimeout there is no need to call clearTimeout unless we want to end the setTimeout early.

https://stackoverflow.com/questions/7391567/when-using-settimeout-do-you-have-to-cleartimeout

And from the HTML5 documentation:

https://www.w3.org/TR/html5/webappapis.html#timers

The clearTimeout() and clearInterval() methods must clear the entry identified as handle from the list of active timers of the WindowOrWorkerGlobalScope object on which the method was invoked, if any, where handle is the argument passed to the method. (If handle does not identify an entry in the list of active timers of the WindowOrWorkerGlobalScope object on which the method was invoked, the method does nothing.)

While this may be true generally, when using setTimeout in React we must account for what happens if a component unmounts before the timer is completed. Imagine we wanted to create a hook that copied text to a user’s clipboard and also provided functionality for changing a button’s text from “Copy to clipboard” to “Copied” and back. We might first take an approach like this:

import { useState } from "react";
import copy from "copy-to-clipboard";

export const useClipboard = () => {
    const innerText = “Copy to clipboard”;

    const [labelText, setLabelText] = useState(innerText);

    const handleCopy = (text: string) => {
        copy(text);
        setLabelText("Copied!");
        setTimeout(() => setLabelText(innerText), 2000);
    };

    return { labelText, handleCopy };
};

Someone using this hook can use the handleCopy function to copy text to a clipboard and can use the labelText to swap the text of a button from ”Copy to clipboard” to “Copied” and back. But what happens if the component that calls the handleCopy function unmounts before 2000ms? There would be a memory leak.

We can fix this by running clearTimeout inside the return of a useEffect which will run when the component unmounts.

import { useState, useEffect } from "react";
import copy from "copy-to-clipboard";

export const useClipboard = () => {
    const innerText = “Copy to clipboard”;

    const [labelText, setLabelText] = useState(innerText);

    useEffect(() => {
        const timer = setTimeout(() => setLabelText(initialInnerText), 2000);
        return () => clearTimeout(timer);
    }, [labelText]);

    const handleCopy = (text: string) => {
        copy(text);
        setLabelText("Copied!");
    };

    return {labelText, handleCopy};
};

While clearTimeout won’t have any effect if the timer is complete, if the component that uses handleCopy unmounts before setTimeout finishes, the return of the useEffect will ensure there is no memory leak.

There’s at least one last optimization we can make to our code. Right now the useEffect runs every time the labelText changes which means we are using setTimeout to swap the labelText even if the text isn’t changing. We can remedy this with a quick return if labelText === innerText.

useEffect(() => {
    if (labelText === innerText) return;

    const timer = setTimeout(() => setLabelText(initialInnerText), 2000);
    return () => clearTimeout(timer);
}, [labelText]);

And that’s a quick example of using setTimeout with clearTimeout in React with hooks!

© 2024 | Alex Fenwood Hughes