While working on a React project recently, I had a need to update some state periodically with data from an API retrieved using fetch(). Coming from a C# background, the way I would approach this problem there would be something like the following:
private async Task FetchDataContinuouslyAsync(CancellationToken cancellationToken)
{
    while (!cancellationToken.IsCancellationRequested)
    {
        await FetchDataAndSetStateAsync(cancellationToken);
        // now wait for 15 seconds before trying again
        await Task.Delay(15000, cancellationToken);
    }
}
Naturally, I went to approach the problem the same way in JavaScript. That's where I hit a snag though - there's no built in function analogous to Task.Delay().
This meant I had to come up with my own solution to the problem. Searching the internet yielded plenty of results where people were using setTimeout along with a Promise, but surprisingly few which supported early cancellation - and those that did tend to return a cancel function rather than observing a token for cancellation. As I was already using fetch() with an AbortController to cancel requests, I wanted to re-use that controller for cancellation.
Here's what I came up with:
/**
 * Return a promise that is resolved after a given delay, or after being cancelled.
 * 
 * @param  {number} duration The delay, in milliseconds.
 * @param  {AbortSignal|null} signal An optional AbortSignal to cancel the delay.
 * 
 * @return {Promise<void>} A promise that is either resolved after the delay, or rejected after the signal is cancelled.
 */
function asyncSleep(duration, signal) {
    function isAbortSignal(val) {
        return typeof val === 'object' && val.constructor.name === AbortSignal.name;
    }
    return new Promise(function (resolve, reject) {
        let timeoutHandle = null;
        function handleAbortEvent() {        
            if (timeoutHandle !== null) {
                clearTimeout(timeoutHandle);
            }
            reject(new DOMException('Sleep aborted', 'AbortError'));
        }
        if (signal !== null && isAbortSignal(signal)) {
            if (signal.aborted) {
                handleAbortEvent();
            }
            signal.addEventListener('abort', handleAbortEvent, {once: true});
        }
        timeoutHandle = setTimeout(function () {
            if (signal !== null && isAbortSignal(signal)) {
                signal.removeEventListener('abort', handleAbortEvent);
            }
            resolve();
        }, duration);
    });
}
This function takes a delay in milliseconds as its first parameter, and an optional AbortSignal as its second parameter. It returns a Promise<void> which will resolve after the specified delay, or be rejected with an AbortError if cancellation is requested.
In the context of a React project, this can be used like the following within a useEffect hook:
useEffect(() => {
    const ac = new AbortController();
    async function fetchDataContinuously(abortController) {
        while (!abortController.signal.aborted) {
            try {
                await getData(abortController.signal);
                await asyncSleep(refreshInterval, abortController.signal);
            } catch (e) {
                if (e.name === 'AbortError') {
                    break;
                }
                console.error('Error continuously refreshing', e);
            }
        }
    }
    fetchDataContinuously(ac).catch(console.error);
    return () => {
        ac.abort();
    };
}, []);
Of course, this could also be used with a traditional class based React component by simply aborting the AbortController in componentWillUnmount as well.
 

 
    
Top comments (0)