How to properly clear fetch timeout in React component?

Viewed 30

I'm fetching Dogs from my API through a JavaScript timeout. It works fine, except it fails to clear the timeout sometimes:

import { useState, useEffect, useCallback } from 'react';

const DogsPage = () => {
    const [dogs, setDogs] = useRef([]);
    const timeoutId = useRef();

    const fetchDogs = useCallback(
        async () => {
            const response = await fetch('/dogs');
            const { dogs } = await response.json();

            setDogs(dogs);

            timeoutId.current = setTimeout(fetchDogs, 1000);
        },
        []
    );

    useEffect(
        () => {
            fetchDogs();

            return () => clearTimeout(timeoutId.current);
        },
        [fetchDogs]
    );

    return <b>Whatever</b>;
};

It looks like the problem is that sometimes I unmount first, while the code is still awaiting for the Dogs to be fetched. Is this a common issue and if so, how would I prevent this problem?

One idea would be to use additional useRef() to keep track of whether the component has been unmounted in between fetch:

const DogsPage = () => {
    const isMounted = useRef(true);

    const fetchDogs = useCallback(
        async () => {
            // My fetching code

            if (isMounted.current) {
                timeoutId.current = setTimeout(fetchDogs, 1000);
            }
        },
        []
    );

    useEffect(
        () => {
            return () => isMounted.current = false;
        },
        []
    );

    // The rest of the code
};

But perhaps there is a cleaner way?

1 Answers

You can assign a sentinel value for timeoutId.current after clearing it, then check for that value before starting a new timer:

import { useState, useEffect, useCallback } from 'react';

const DogsPage = () => {
    const [dogs, setDogs] = useRef([]);
    const timeoutId = useRef();

    const fetchDogs = useCallback(
        async () => {
            const response = await fetch('/dogs');
            const { dogs } = await response.json();

            setDogs(dogs);

            if (timeoutId.current !== -1)
                timeoutId.current = setTimeout(fetchDogs, 1000);
        },
        []
    );

    useEffect(
        () => {
            fetchDogs();

            return () => void (clearTimeout(timeoutId.current), timeoutId.current = -1);
        },
        [fetchDogs]
    );

    return <b>Whatever</b>;
}; 
Related