AbortController polyfill for cancelling fetch()
While writing tests for a React app I ran into the following warning:
Warning: setState(...): Can only update a mounted or mounting component. This usually
means you called setState() on an unmounted component. This is a no-op. Please check
the code for the BoatyMcBoatface component.
This warning is emitted, for example, when a component starts a REST call (or timer etc) and then gets unmounted before the call completes. Once the call does complete, it typically tries to do setState() on the component, which is not legal after it has been unmounted.
So the correct fix is to abort the REST call when the component unmounts, which is easy if you’re
using axios or some other XMLHttpRequest
wrapper. The problem was that I was using native
fetch()
which doesn’t have support for aborting the request (yet). I googled around for the
status on cancellation and it turns out that just a few days ago a new API was been
added1 to the DOM specification for this. This new API is a smaller scope
compared to cancellation of promises for which discussions stalled a bit early this year.
This new DOM API allows you to create an
AbortController
that in turn allows you to pass an AbortSignal
into the fetch()
call. Later on you can call .abort()
on the controller to cancel the request. If you used the
new API from a React application, it would look like this:
componentDidMount() {
this.controller = new AbortController();
const signal = this.controller.signal;
fetch(`/api/thing?id=${this.props.thingId}`, {signal}).then(res => {
if (res.status == 400) {
this.setState({ loading: false, actualThing: undefined });
return;
}
return res.json();
}).then(actualThing => {
this.setState({ loading: false, actualThing });
}).catch(err => {
if (err.name == 'AbortError') {
return;
}
// It's important to rethrow all other errors so you don't silence them!
// For example, any error thrown by setState(), will pass through here.
throw err;
});
}
componentWillUnmount() {
this.controller.abort();
}
I wanted to try this new API in my application, so I created a polyfill for it. The polyfill does
not actually close the TCP connection on abort, it just throws the AbortError
as seen above
and then when the response arrives it is dropped silently (the real implementation will close the
TCP socket though of course). This allows you to target the new API today and once the API is
implemented in browsers you can just drop the polyfill and not modify your code. The polyfill is
available here:
If you’re using webpack, you simply npm install --save abortcontroller-polyfill
and then
import 'abortcontroller-polyfill';
or require('abortcontroller-polyfill');
near the top
of your client entrypoint .js or just include 'abortcontroller-polyfill'
as an entrypoint
itself (before your own entrypoint).
When I was googling around for the status on cancellation, I also noticed that Ron Buckton is presenting a very ambitious proposal (slides) at a TC39 meeting (which happens to be today!) that aims to unify cancellation, not just for DOM but for asynchronous functions, asynchronous iterators, webworkers, animation etc in such a way that they can be both synchronous and asynchronously observed (i.e. asking “was it cancelled?” verus “tell me if it cancels”). This work is based on cancellation in managed threads in the .NET Framework.
1: The final version of AbortController has been added to the DOM specification. The corresponding PR for the fetch specification is essentially done but not technically merged yet, waiting for testcases to be merged into web-platform-tests via PR #6484. Browser bugs tracking the implementation of AbortController is available here: Firefox: #1378342, Chromium: #750599, WebKit: #174980, Edge: #13009916.