Carl Rippon

Building SPAs

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

Cancelling Requests with React Query

April 21, 2021
reacttypescript

This is another post in a series of posts using React Query with TypeScript.

Previous posts:

The post covers how to cancel fetch requests with React Query. Our implementation will allow React Query to cancel a request for us if it is in-flight when its component is unmounted. We will also enable the user to click a button to cancel the request.

Cancelling reqeusts

Our start point

We will start with code similar to what we finished in the Getting started post.

This is what the fetching function looks like:

type Character = {
  name: string;
};
type Params = {
  queryKey: [string, { id: number }];
};
async function getCharacter(params: Params) {
  const [, { id }] = params.queryKey;
  const response = await fetch(`https://swapi.dev/api/people/${id}/`);
  if (!response.ok) {
    throw new Error("Problem fetching data");
  }
  const character = await response.json();
  assertIsCharacter(character);

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

This is the component containing the query:

function CharacterDetails() {
  const { status, error, data } = useQuery<Character, Error>(
    ["character", { id: 1 }],
    getCharacter
  );

  if (status === "loading") {
    return <button>Cancel</button>;
  }
  if (status === "error") {
    return <div>{error!.message}</div>;
  }
  return data ? <h3>{data.name}</h3> : null;
}

There is a Cancel button that is rendered while the data is being fetched. When this button is clicked, we want to cancel the query.

Providing a method to cancel the request

A previous post covered how a fetch request can be cancelled with AbortController. This contains a signal property that can be passed to fetch and an abort method that can then be used to cancel the request.

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

interface PromiseWithCancel<T> extends Promise<T> {
  cancel: () => void;
}
function getCharacter(params: Params) {
  const [, { id }] = params.queryKey;
  const controller = new AbortController();
  const signal = controller.signal;
  const promise = new Promise(async (resolve, reject) => {
    try {
      const response = await fetch(`https://swapi.dev/api/people/${id}/`, {
        method: "get",
        signal,
      });
      if (!response.ok) {
        reject(new Error("Problem fetching data"));
      }
      const data = await response.json();
      assertIsCharacter(data);
      resolve(data);
    } catch (ex: unknown) {
      if (isAbortError(ex)) {
        reject(new Error("Request cancelled"));
      }
    }
  });
  (promise as PromiseWithCancel<Character>).cancel = () => {
    controller.abort();
  };
  return promise as PromiseWithCancel<Character>;
}
function isAbortError(error: any): error is DOMException {
  if (error && error.name === "AbortError") {
    return true;
  }
  return false;
}

We have wrapped the existing code in a new Promise. The Promise is resolved with the data from the request. If the request is cancelled or is unsuccessful, the promise is rejected with an appropriate error.

We’ve added a cancel method to the returned Promise, which calls AbortController.abort. We have added the PromiseWithCancel interface and used this on the returned promised, otherwise a type error would occur.

Cancelling the query

There is a cancelQueries function that can be called on the React Query client to cancel requests.

First, we need to get a reference to the query client. We can do this using the useQueryClient hook:

import { useQuery, useQueryClient } from "react-query";...
function CharacterDetails() {
  const queryClient = useQueryClient();  ...
}

The cancelQueries function can then be used on the button click event:

function CharacterDetails() {
  ...
  if (status === "loading") {
    return (
      <button onClick={() => queryClient.cancelQueries("character")}>        Cancel
      </button>
    );
  }
  ...
}

The query’s key to cancel is passed into cancelQueries, which is "character" in our example.

If we give this a try on a slow connection, we will see the query is cancelled when the cancel button is clicked:

Manual cancel

Nice. 😊

Cancelling in-flight requests when its component is umounted

By default, React Query doesn’t cancel in-flight requests when its component is umounted. However, if the fetching function’s returned promise contains a cancel method, it will invoke this to cancel an in-flight query when its component is unmounted.

Let’s give this a try by removing the component from the rendering tree after 1 second:

export default function App() {
  const [renderComponent, setRenderComponent] = React.useState(true);
  React.useEffect(() => {
    setTimeout(() => {
      setRenderComponent(false);
    }, 1000);
  }, []);
  return <div className="App">{renderComponent && <CharacterDetails />}</div>;
}

On a slow connection, the fetch request will still be in-flight after 1 second. Let’s see what happens:

Auto cancel

The query is cancelled after 1 second without the Cancel button being clicked.

Cool. 😊

The code in this post is available in CodeSandbox at https://codesandbox.io/s/react-query-cancel-33jgk?file=/src/App.tsx

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