Cancelling Requests with React Query
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.
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:
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:
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.If you to learn more about using TypeScript with React, you may find my course useful: