Carl Rippon

Building SPAs

Carl Rippon
BlogBooks / CoursesAbout
This site uses cookies. Click here to find out more

Cancelling fetch in React and TypeScript

February 24, 2021
reacttypescript

This post will cover how to programmatically cancel a fetch request in a React and TypeScript app.

Cancelling fetch with TypeScript

A React component

We have a typical React component that fetches some data from a web API and renders it:

export function App() {
  const [status, setStatus] = React.useState<"loading" | "loaded" | "cancelled">("loading");
  const [data, setData] = React.useState<Character | undefined>(undefined);

  React.useEffect(() => {
    getCharacter(1).then((character) => {
      setData(character);
      setStatus("loaded");
    });
  }, []);

  if (status === "loading") {
    return (
      <div>
        <div>loading ...</div>
        <button>Cancel</button>
      </div>
    );
  }
  if (status === "cancelled") {
    return <div>Cancelled</div>;
  }

  return <div>{data && <h3>{data.name}</h3>}</div>;
}

The data is fetched inside the getCharacter function inside a useEffect and put in state called data.

A state variable called status tracks where we are in the fetching process. Notice that a Cancel button is being rendered when the data is being fetched.

Cancel button

When the Cancel button is clicked, we want to cancel the fetch request.

Let’s have a look at the getCharacter function:

async function getCharacter(id: number) {
  const response = await fetch(`https://swapi.dev/api/people/${id}/`);
  const data = await response.json();
  assertIsCharacter(data);
  return data;
}

It’s a straightforward request to the Star Wars API.

Here’s the Character type:

type Character = {
  name: string;
};

We are only interested in the name field from the Star Wars people resource.

The assertIsCharacter type assert function is as follows:

function assertIsCharacter(data: any): asserts data is Character {
  if (!("name" in data)) {
    throw new Error("Not character");
  }
}

This type assert function allows TypeScript to narrow the type of data to Character.

Using AbortController to cancel fetch

AbortController is a fairly recent addition to JavaScript which came after the initial fetch implementation. The good news is that it is supported in all modern browsers.

AbortController contains an abort method. It also contains a signal property that can be passed to fetch. When AbortController.abort is called, the fetch request is cancelled.

Let’s use AbortController and its signal in the fetch request in getCharacter:

function getCharacter(id: number) {
  const controller = new AbortController();
  const signal = controller.signal;
  const promise = new Promise(async (resolve) => {
    const response = await fetch(`https://swapi.dev/api/people/${id}/`, {
      method: "get",
      signal,
    });
    const data = await response.json();
    assertIsCharacter(data);
    resolve(data);
  });
  promise.cancel = () => controller.abort();
  return promise;
}

We have removed the async keyword from getCharacter and wrapped the existing code in a new Promise. The Promise is resolved with the data from the request. We’ve added a cancel method to the Promise, which calls AbortController.abort.

The Promise containing the cancel method is returned from getCharacter so that the calling code can use this to cancel the request.

We have a type error though:

// 💥 - Property 'cancel' does not exist on type 'Promise<unknown>'
promise.cancel = () => controller.abort();

Let’s create a new type for the Promise containing the cancel method:

interface PromiseWithCancel<T> extends Promise<T> {
  cancel: () => void;
}

We can then use a type assertion to resolve the type error:

function getCharacter(id: number) {
  ...
  (promise as PromiseWithCancel<Character>).cancel = () => controller.abort();  return promise as PromiseWithCancel<Character>;}

Using the new getCharacter in the React component

We are going to store the promise from getCharacter in a state variable called query.

export function App() {
  const [status, setStatus] = React.useState<"loading" | "loaded" | "cancelled">("loading");
  const [data, setData] = React.useState<Character | undefined>(undefined);
  const [query, setQuery] = React.useState<PromiseWithCancel<Character> | undefined>(undefined);  React.useEffect(() => {
    const q = getCharacter(1);    setQuery(q);    q.then((character) => {      setData(character);
      setStatus("loaded");
    });
  }, []);
  ...

We can now call the cancel method in the promise when the Cancel button is clicked:

<button
  onClick={() => {    query?.cancel();    setStatus("cancelled");  }}>
  Cancel
</button>

When the Cancel button is clicked, we see that Cancelled is rendered:

Cancel button

If we look at the network requests, we can see the request was cancelled:

Cancelled request

Nice. 😊

Catching the abort error

If we look at the console, we see that an error was raised when the request was cancelled:

Cancel error

We can catch this error by wrapping the request in a try catch statement:

const promise = new Promise(async (resolve) => {
  try {    const response = await fetch(`https://swapi.dev/api/people/${id}/`, {
      method: "get",
      signal,
    });
    const data = await response.json();
    assertIsCharacter(data);
    resolve(data);
  } catch (ex: unknown) {    if (isAbortError(ex)) {      console.log(ex.message);    }  }});

The isAbortError type predicate function is as follows:

function isAbortError(error: any): error is DOMException {
  if (error && error.name === "AbortError") {
    return true;
  }
  return false;
}

Now when we click the Cancel button, we get the message output to the console rather than the error:

Cancel message

Wrap up

The signal property in AbortController can be passed into fetch. AbortController.abort can be then called to cancel the request.

Cancelling fetch raises an error that can be swallowed using try catch.

The complete code is in this gist.

Did you find this post useful?

Let me know by sharing it on Twitter.
Click here to share this post on Twitter

If you to learn more about using TypeScript with React, you may find my course useful:

Using TypeScript with React

Find out more