Using AbortControllers to Cancel Fetch in React

How to use the web api to cancel fetch easily

12 Apr 2021

In React we can use fetch in a useEffect hook to make a request when the page loads. So why is the code below problematic?

const Example = () => {
  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/posts/1")
      .then((res) => res.json())
      .then((json) => setMessage(json.title))
      .catch((error) => console.error(error.message));
  }, []);
  return <div>{message}</div>;
};

If we unmount the component while this fetch is in process we will get the following error.

Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount
method.

What does this mean? update on an unmounted component summarizes it well. We are trying to change the state of a component that no longer exists, because it was removed. This can lead to our app breaking in weird ways, as well as wasting resources on unnecessary actions. So let's fix it by canceling the fetch.

useEffect(() => {
  const controller = new AbortController();

  fetch("https://jsonplaceholder.typicode.com/posts/1", {
    signal: controller.signal,
  })
    .then((res) => res.json())
    .then((json) => setMessage(json.title))
    .catch((error) => console.error(error.message));

  return () => controller.abort();
}, []);

Here we use the web api AbortController as the signal for fetch. By returning a function from useEffect we can trigger the abort controller on dismount (see the React docs). The AbortSignal (controller.signal) is then passed into the fetch as an argument and voilĂ !

Although, there is a problem with this solution. When the component is unmounted while a fetch call is in progress, this message is logged to the console:

The user aborted a request.

This happens because aborting the fetch doesn't magically delete the promise, so it fails with an AbortError which is getting logged in catch.

useEffect(() => {
  const controller = new AbortController();

  fetch("https://jsonplaceholder.typicode.com/posts/1", {
    signal: controller.signal,
  })
    .then((res) => res.json())
    .then((json) => setMessage(json.title))
    .catch((error) => {
      if (error.name !== "AbortError") {
        console.error(error.message);
      }
    });

  return () => controller.abort();
}, []);

By ignoring the error if it has the name AbortError, we've solved the problem! You can read more about AbortController on MDN. You can also see its browser support on caniuse.

Jumping back to the code, this request this still doesn't expose a loading state or indicate to the ui if it has failed. If you don't want to deal with all of this, you're probably better off just using react-query instead (which also does it in a lot less code).

const { data, isLoading /* etc... */ } = useQuery("title", () =>
  fetch("https://jsonplaceholder.typicode.com/posts/1")
    .then((res) => res.json())
    .then((res) => res.title),
);

How much easier is that! You can find an example of all of these methods implemented on CodeSandbox.

All Posts