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)