When in doubt, signal!
… and check why 5600+ Rails engineers read also this
When you are writing frontend applications you often need to fetch some data. Sometimes, especially when you’re working with a single-page app, some requests could be cancelled as you would not use the response anywhere, e.g. when your visitor clicks on some link and changes the page during the pending request that would no longer be needed on the next page.
If you are using fetch
to perform AJAX requests, you can use AbortController
to cancel pending requests in such situations.
AbortController
and AbortSignal
are features available in most modern browsers, there are also some polyfills
available for those unfortunates who need to support IE.
Usage
Using AbortController
to cancel fetch
requests is easy. You just need to create a controller instance and pass it’s signal
property to fetch
. Then, when you need to abort a request, you just need to call the abort()
method:
const controller = new AbortController();
const request = fetch("/api/something", {signal: controller.signal});
// ...
controller.abort()
Once you call controller.abort()
, the signal passed to the fetch
call will cancel the request and throw an AbortError
. You can catch it using regular try {} catch (e) {}
block:
try {
performCancellableRequest()
} catch (error) {
if (error.name === "AbortError") return; // ignore cancelled requests
errorReporting.notify(error);
}
React hook
If you are using React in your application, you can write a simple hook that will encapsulate all logic related with AbortController
for easier request handling:
import {useEffect} from "react";
export default function useSignal(dependencies = []) {
const controller = new AbortController();
useEffect(() => () => controller.abort(), dependencies);
return controller.signal;
}
This hook could be used in your function component that is fetching some data. When such component is unmounted, all pending requests will be cancelled. You should remember to handle the AbortError
in your state management so that you won’t update the state when your component is unmounted. An example component might look like this:
import {useCallback, useEffect, useState} from "react";
import useSignal from "./useSignal";
export default function Profile() {
const [profile, setProfile] = useState(null);
const signal = useSignal();
const fetchData = useCallback(async () => {
try {
const response = await fetch("/api/profile", {signal});
setProfile(await response.json());
} catch (error) {
if (error.name !== "AbortError") throw error;
}
}, [])
useEffect(() => {
fetchData();
}, []);
if (!profile) return <div>Loading...</div>;
return <div>Your name: {profile.name}</div>;
}
With such implementation you’ll fetch the data on initial render and call setProfile
only if the request was successful. If you unmount the component, the request will be cancelled an AbortError
will be catched and ignored (so you won’t try to update the state of an already unmounted component). As we are throwing all errors other than AbortError
, you should wrap your component with an Error Boundary
to prevent crashes in case of any other error that might occur during fetching the data.