On frontend, you may have a piece of code that fires network requests very quickly, based on user interaction. Debouncing such actions becomes less and less a viable option because of user experience. Especially with advent of React.js Suspense which deals with this and many other things on a such fine low level. But what if you want to fix such behavior in just a few places, without being reliant on UI libraries. redux-saga has it's methods to battle race conditions but it's tightly related to redux actions. Native window.fetch with AbortController offers network request cancellation mechanism, but is not supported by the older browsers. Maybe there is more than just network requests you want to guard from race conditions?!
In most cases, it just boils down to how your code reacts to resolved promises.
Previously we mentioned an util function that enables cancellation of Promise's success chain. Let's just paste that code here for reference, as we will use it to construct our takeLatest(promise)
util function:
const cancelable = (promise) => {
let hasCancelled = false;
return {
promise: promise.then(v => {
if (hasCancelled) {
throw { isCancelled: true };
}
return v;
}),
cancel: () => hasCancelled = true
}
};
Now, we'll want to create another util function that'll keep track of the previously passed in promise, cancel it if it hasn't finished yet and replace with the new one:
const createTakeLatest = () => {
let cancelablePromise = null;
const takeLatest = promise => {
if (cancelablePromise) {
cancelablePromise.cancel();
cancelablePromise = cancelable(promise);
} else {
cancelablePromise = cancelable(promise);
}
return cancelablePromise.promise;
};
return takeLatest;
};
Now we can generate ourselves a tracker of sorts that receives promises, and allows only the last one to finish(if we have multiple concurrent request):
const takeLatest = createTakeLatest()
// imagine having fetchPage async function implemented to fetch a page from a server
for (let i = 0; i < 20; i++) {
const request = fetch(`/api/someData?page=${i}`)
takeLatest(request)
.then(() => console.log('Page fetched'))
.catch(error => {
if (!error.isCancelled) {
// handle only requests that haven't been cancelled
}
})
}
In the example above, only the last request's handlers will be executed, all the rest will get cancelled and caught as errors with isCancelled flag set.
Top comments (0)