Debounced fetch with Abort Controller
1. Being a good API consumer
The point of debouncing is to not hammer the endpoint we're calling with tons of requests we're going to discard anyway.
2. Saving on our users' data bills
We want to be respectful of our users' data plans by not having them download data that is not needed.
3. Avoiding race conditions between multiple in-flight requests
Requests made in a certain order are not guaranteed to be received in that same order. If we can avoid dealing with that complexity, our application becomes easier to maintain.
Without further ado, here's a little demo you can try out. Just type anything in the box, then, while it's loading, continue typing. If you're on desktop you can have a look in the devtools as well to see the requests being canceled when you resume typing while the request is still pending.
This is a great use-case for the AbortController browser API. The following code sample is written in React but the same principles apply to any library or framework you're using (if any).
First off, we need to store a stable reference to the debounce timer and the AbortController
itself, so bring out useRef
.
const debounceTimer = useRef(0);
const controller = useRef();
In our debounce function, we first clear our timer, then proceed to invoke the controller's abort
method and immediately assign it to a new AbortController
. This is because once abort
has been called, that instance of AbortController
is used up.
const debouncedFetch = (orderNumber) => {
clearTimeout(debounceTimer.current);
controller.current?.abort();
controller.current = new AbortController();
When making our API call, we send along the signal
property in our fetch options, set to the controller's signal. This associates our AbortController
with this particular request, and makes it so that the call to abort
above actually does something.
debounceTimer.current = setTimeout(async () => {
try {
setLoading(true);
const order = await fetch(`/api/orders?orderNo=${orderNumber}`, {
method: 'GET',
signal: controller.current.signal
});
setOrder(order);
} catch (err) {
if (err instanceof Error && err.name !== 'AbortError') {
// Handle any error not caused by the Abort Controller aborting the request
}
} finally {
setLoading(false);
}
}, debounceDelay);
};
One thing to note above is that the AbortController
's cancellation of the request is technically considered an error, meaning you'll have to inspect the name
property of the error in your catch block, to see if the error is an AbortError
. You don't want to show any error messages to the user when the request is canceled.
To see it all put together, get the complete code for the demo in this post in this Gist.