Canceling Requests in React/React Native: A Comprehensive Guide
In a world of async-heavy apps, canceling unnecessary or outdated requests is key to improving performance and user experience. Imagine you're typing into a search field and triggering a request for every keystroke—without canceling the previous one, you'll be wasting resources and potentially displaying outdated results. This article dives into various ways to handle cancellation of requests in React and React Native, covering: Axios: Using CancelToken to cancel requests. Fetch: Leveraging the AbortController API. React Query: Built-in cancellation support. RTK Query: Using AbortController for cancellation. React Native: Handling cancellation in a mobile context. Conclusion: Summarizing the key points and best practices. References: Links to official documentation and further reading. Code Snippets: Examples for each method. Live Examples: Interactive code snippets to test and play with. Table of Contents Links Canceling Requests with Axios Canceling Requests with Fetch Canceling Requests with React Query Canceling Requests with RTK Query Canceling Requests with React Native Conclusion References Links Full Code Live Example Canceling Requests with Axios Axios is a popular HTTP client for JavaScript, and it provides a built-in way to cancel requests using CancelToken. Here's how you can implement it: import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { useToast } from "@/hooks/use-toast"; import axios, { CancelTokenSource } from "axios"; import { LoaderIcon, Receipt } from "lucide-react"; import { useEffect, useRef, useState } from "react"; type Todo = { userId: number; id: number; title: string; completed: boolean; }; const CancelToken = axios.CancelToken; async function getPosts( url: string, cancelToken: CancelTokenSource ): Promise { const response = await axios.get(url, { cancelToken: cancelToken.token, }); return response.data; } const AxiosCancelTokenExample = () => { const { toast } = useToast(); const cancelToken = useRef(null); const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(false); const handleError = (err: unknown) => { let message = `[UNKNOWN_ERROR ${new Date().toISOString()}] ${JSON.stringify( err, Object.getOwnPropertyNames(err) )}`; if (axios.isCancel(err)) { message = `[REQUEST_CANCELLED ${new Date().toISOString()}] ${ err.message }`; } else if (axios.isAxiosError(err)) { message = `[AXIOS_ERROR ${new Date().toISOString()}] ${err.message}`; } else if (err instanceof Error) { message = `[GENERIC_ERROR ${new Date().toISOString()}] ${err.message}`; } toast({ title: "Error", description: message, variant: "destructive", draggable: true, duration: 5000, }); }; const fetchPosts = async () => { const source = CancelToken.source(); cancelToken.current = source; setPosts([]); setLoading(true); try { const response = await getPosts( "https://jsonplaceholder.typicode.com/posts", source ); setPosts(response); } catch (err) { handleError(err); } finally { setLoading(false); } }; const createTodo = async (todo: Omit) => { const source = CancelToken.source(); cancelToken.current = source; setLoading(true); try { const response = await axios.post( "https://jsonplaceholder.typicode.com/posts", todo, { cancelToken: source.token, } ); setPosts((prev) => [...prev, response.data]); } catch (err) { handleError(err); } finally { setLoading(false); } }; const patchTodo = async (todo: Todo) => { const source = CancelToken.source(); cancelToken.current = source; setLoading(true); try { const response = await axios.patch( `https://jsonplaceholder.typicode.com/posts/${todo.id}`, todo, { cancelToken: source.token, } ); setPosts((prev) => prev.map((item) => item.id === response.data.id ? response.data : item ) ); } catch (err) { handleError(err); } finally { setLoading(false); } }; const deleteTodo = async (todoId: number) => { const source = CancelToken.source(); cancelToken.current = source; setLoading(true); try { await axios.delete( `https://jsonplaceholder.typicode.com/posts/${todoId}`, { cancelToken: source.token, } ); setPosts((prev) => prev.filter((item) => item.id !== todoId)); } catch (err) { handleError(err); } finally { setLoading(false); } }; const handleCancel = () => { if (cancelToken.current) { cancelToken.current.cancel

In a world of async-heavy apps, canceling unnecessary or outdated requests is key to improving performance and user experience. Imagine you're typing into a search field and triggering a request for every keystroke—without canceling the previous one, you'll be wasting resources and potentially displaying outdated results.
This article dives into various ways to handle cancellation of requests in React and React Native, covering:
-
Axios: Using
CancelToken
to cancel requests. -
Fetch: Leveraging the
AbortController
API. - React Query: Built-in cancellation support.
-
RTK Query: Using
AbortController
for cancellation. - React Native: Handling cancellation in a mobile context.
- Conclusion: Summarizing the key points and best practices.
- References: Links to official documentation and further reading.
- Code Snippets: Examples for each method.
- Live Examples: Interactive code snippets to test and play with.
Table of Contents
- Links
- Canceling Requests with Axios
- Canceling Requests with Fetch
- Canceling Requests with React Query
- Canceling Requests with RTK Query
- Canceling Requests with React Native
- Conclusion
- References
Links
Canceling Requests with Axios
Axios is a popular HTTP client for JavaScript, and it provides a built-in way to cancel requests using CancelToken
. Here's how you can implement it:
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { useToast } from "@/hooks/use-toast";
import axios, { CancelTokenSource } from "axios";
import { LoaderIcon, Receipt } from "lucide-react";
import { useEffect, useRef, useState } from "react";
type Todo = {
userId: number;
id: number;
title: string;
completed: boolean;
};
const CancelToken = axios.CancelToken;
async function getPosts<T>(
url: string,
cancelToken: CancelTokenSource
): Promise<T> {
const response = await axios.get<T>(url, {
cancelToken: cancelToken.token,
});
return response.data;
}
const AxiosCancelTokenExample = () => {
const { toast } = useToast();
const cancelToken = useRef<CancelTokenSource | null>(null);
const [posts, setPosts] = useState<Todo[]>([]);
const [loading, setLoading] = useState(false);
const handleError = (err: unknown) => {
let message = `[UNKNOWN_ERROR ${new Date().toISOString()}] ${JSON.stringify(
err,
Object.getOwnPropertyNames(err)
)}`;
if (axios.isCancel(err)) {
message = `[REQUEST_CANCELLED ${new Date().toISOString()}] ${
err.message
}`;
} else if (axios.isAxiosError(err)) {
message = `[AXIOS_ERROR ${new Date().toISOString()}] ${err.message}`;
} else if (err instanceof Error) {
message = `[GENERIC_ERROR ${new Date().toISOString()}] ${err.message}`;
}
toast({
title: "Error",
description: message,
variant: "destructive",
draggable: true,
duration: 5000,
});
};
const fetchPosts = async () => {
const source = CancelToken.source();
cancelToken.current = source;
setPosts([]);
setLoading(true);
try {
const response = await getPosts<Todo[]>(
"https://jsonplaceholder.typicode.com/posts",
source
);
setPosts(response);
} catch (err) {
handleError(err);
} finally {
setLoading(false);
}
};
const createTodo = async (todo: Omit<Todo, "id">) => {
const source = CancelToken.source();
cancelToken.current = source;
setLoading(true);
try {
const response = await axios.post<Todo>(
"https://jsonplaceholder.typicode.com/posts",
todo,
{
cancelToken: source.token,
}
);
setPosts((prev) => [...prev, response.data]);
} catch (err) {
handleError(err);
} finally {
setLoading(false);
}
};
const patchTodo = async (todo: Todo) => {
const source = CancelToken.source();
cancelToken.current = source;
setLoading(true);
try {
const response = await axios.patch<Todo>(
`https://jsonplaceholder.typicode.com/posts/${todo.id}`,
todo,
{
cancelToken: source.token,
}
);
setPosts((prev) =>
prev.map((item) =>
item.id === response.data.id ? response.data : item
)
);
} catch (err) {
handleError(err);
} finally {
setLoading(false);
}
};
const deleteTodo = async (todoId: number) => {
const source = CancelToken.source();
cancelToken.current = source;
setLoading(true);
try {
await axios.delete(
`https://jsonplaceholder.typicode.com/posts/${todoId}`,
{
cancelToken: source.token,
}
);
setPosts((prev) => prev.filter((item) => item.id !== todoId));
} catch (err) {
handleError(err);
} finally {
setLoading(false);
}
};
const handleCancel = () => {
if (cancelToken.current) {
cancelToken.current.cancel("Request canceled by the user.");
}
};
const replaceTodo = async (todo: Todo) => {
const source = CancelToken.source();
cancelToken.current = source;
setLoading(true);
try {
const response = await axios.put<Todo>(
`https://jsonplaceholder.typicode.com/posts/${todo.id}`,
todo,
{
cancelToken: source.token,
}
);
setPosts((prev) =>
prev.map((item) =>
item.id === response.data.id ? response.data : item
)
);
} catch (err) {
handleError(err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchPosts();
return () => {
if (cancelToken.current) {
cancelToken.current.cancel("Request canceled by the user.");
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className="flex flex-col gap-4 mt-4">
<header className="flex flex-row items-center justify-between w-full gap-4">
<div />
<Label size={"3xl"} weight={"bold"} className="text-center uppercase">
Axios Cancel Token Example
Label>
<Button
onClick={() => {
const todo = {
userId: 1,
title: "New Todo",
completed: false,
};
createTodo(todo);
}}
disabled={loading}
>
Create Todo
Button>
header>
<Alert variant={"info"}>
<AlertTitle className="font-bold">Abort RequestAlertTitle>
<AlertDescription>
This example demonstrates how to use Axios Cancel Token to abort a
request. Click the "Cancel Request" button to cancel the request. The
request will be aborted, and an error message will be displayed in the
toast notification. The request will be retried automatically after 5
seconds. You can test with network throttling in the browser developer
tools to simulate a slow network and see the cancel token in action.
AlertDescription>
Alert>
<div className="flex gap-4 *:w-full">
<Button disabled={!loading} onClick={handleCancel}>
Cancel Request
Button>
<Button
disabled={loading}
aria-label="Retry Request"
variant="outline"
onClick={fetchPosts}
>
Retry Request
Button>
div>
{loading && (
<div className="flex flex-col items-center gap-4">
<p className="text-center">Loading...p>
<div className="flex justify-center">
<LoaderIcon className="animate-spin text-primary" />
div>
div>
)}
<ul className="max-h-[400px] px-2 overflow-y-auto *:border-b border-b-primary">
{posts.map((post) => (
<li
key={post.id}
className="flex items-center justify-between w-full border-b last:border-none hover:bg-gray-100"
>
<div className="flex items-center w-full gap-2">
<Receipt className="inline-block" />
{post.title}
div>
<Button
variant="destructive"
className="inline-block p-1 m-0 ml-2 h-fit"
onClick={() => deleteTodo(post.id)}
>
Delete
Button>
<Button
variant="outline"
className="inline-block p-1 m-0 ml-2 h-fit"
onClick={() =>
patchTodo({ ...post, title: `${post.title} (Updated)` })
}
>
Update
Button>
<Button
variant="secondary"
className="inline-block p-1 m-0 ml-2 h-fit"
onClick={() =>
replaceTodo({
...post,
title: `Replaced Todo`,
completed: true,
})
}
>
Replace
Button>
li>
))}
ul>
div>
);
};
export default AxiosCancelTokenExample;
Explanation
CancelToken: Axios provides a
CancelToken
that can be used to cancel requests. You create a new token usingCancelToken.source()
and pass it to the request.Canceling Requests: You can cancel a request by calling
cancelToken.current.cancel("Request canceled by the user.")
. This will trigger thecatch
block in the request, where you can handle the cancellation.Error Handling: The
handleError
function checks if the error is a cancellation error or an Axios error and displays an appropriate message.Cleanup: In the
useEffect
cleanup function, we cancel any ongoing requests when the component unmounts.Creating Todos: The
createTodo
function demonstrates how to create a new todo item using a POST request. It also uses the cancel token to allow cancellation of the request.
Canceling Requests with Fetch
The Fetch API does not have built-in cancellation support, but you can use the AbortController
to achieve similar functionality. Here's how you can implement it:
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { useToast } from "@/hooks/use-toast";
import { LoaderIcon, Receipt } from "lucide-react";
import { useEffect, useRef, useState } from "react";
type Todo = {
userId: number;
id: number;
title: string;
completed: boolean;
};
const FetchAbortControllerExample = () => {
const { toast } = useToast();
const abortController = useRef<AbortController | null>(null);
const [posts, setPosts] = useState<Todo[]>([]);
const [loading, setLoading] = useState(false);
const handleError = (err: unknown) => {
let message = `[UNKNOWN_ERROR ${new Date().toISOString()}] ${JSON.stringify(
err,
Object.getOwnPropertyNames(err)
)}`;
if (err instanceof DOMException && err.name === "AbortError") {
message = `[REQUEST_ABORTED ${new Date().toISOString()}] ${err.message}`;
} else if (err instanceof Error) {
message = `[FETCH_ERROR ${new Date().toISOString()}] ${err.message}`;
}
toast({
title: "Error",
description: message,
variant: "destructive",
draggable: true,
duration: 5000,
});
};
const fetchPosts = async () => {
const controller = new AbortController();
abortController.current = controller;
setPosts([]);
setLoading(true);
try {
const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
signal: controller.signal,
});
const data = await res.json();
setPosts(data);
} catch (err) {
handleError(err);
} finally {
setLoading(false);
}
};
const createTodo = async (todo: Omit<Todo, "id">) => {
const controller = new AbortController();
abortController.current = controller;
setLoading(true);
try {
const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
body: JSON.stringify(todo),
headers: { "Content-Type": "application/json" },
signal: controller.signal,
});
const data = await res.json();
setPosts((prev) => [...prev, data]);
} catch (err) {
handleError(err);
} finally {
setLoading(false);
}
};
const patchTodo = async (todo: Todo) => {
const controller = new AbortController();
abortController.current = controller;
setLoading(true);
try {
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${todo.id}`,
{
method: "PATCH",
body: JSON.stringify(todo),
headers: { "Content-Type": "application/json" },
signal: controller.signal,
}
);
const data = await res.json();
setPosts((prev) =>
prev.map((item) => (item.id === data.id ? data : item))
);
} catch (err) {
handleError(err);
} finally {
setLoading(false);
}
};
const replaceTodo = async (todo: Todo) => {
const controller = new AbortController();
abortController.current = controller;
setLoading(true);
try {
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${todo.id}`,
{
method: "PUT",
body: JSON.stringify(todo),
headers: { "Content-Type": "application/json" },
signal: controller.signal,
}
);
const data = await res.json();
setPosts((prev) =>
prev.map((item) => (item.id === data.id ? data : item))
);
} catch (err) {
handleError(err);
} finally {
setLoading(false);
}
};
const deleteTodo = async (todoId: number) => {
const controller = new AbortController();
abortController.current = controller;
setLoading(true);
try {
await fetch(`https://jsonplaceholder.typicode.com/posts/${todoId}`, {
method: "DELETE",
signal: controller.signal,
});
setPosts((prev) => prev.filter((item) => item.id !== todoId));
} catch (err) {
handleError(err);
} finally {
setLoading(false);
}
};
const handleCancel = () => {
if (abortController.current) {
abortController.current.abort();
}
};
useEffect(() => {
fetchPosts();
return () => {
abortController.current?.abort();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className="flex flex-col gap-4 mt-4">
<header className="flex flex-row items-center justify-between w-full gap-4">
<div />
<Label size={"3xl"} weight={"bold"} className="text-center uppercase">
Fetch Abort Controller Example
Label>
<Button
onClick={() =>
createTodo({ userId: 1, title: "New Todo", completed: false })
}
disabled={loading}
>
Create Todo
Button>
header>
<Alert variant={"info"}>
<AlertTitle className="font-bold">Abort RequestAlertTitle>
<AlertDescription>
This example demonstrates how to use Fetch AbortController to cancel a
request. Click "Cancel Request" to abort an ongoing request. Try
network throttling in dev tools to simulate a slow network and test
cancellation.
AlertDescription>
Alert>
<div className="flex gap-4 *:w-full">
<Button disabled={!loading} onClick={handleCancel}>
Cancel Request
Button>
<Button
disabled={loading}
aria-label="Retry Request"
variant="outline"
onClick={fetchPosts}
>
Retry Request
Button>
div>
{loading && (
<div className="flex flex-col items-center gap-4">
<p className="text-center">Loading...p>
<LoaderIcon className="animate-spin text-primary" />
div>
)}
<ul className="max-h-[400px] px-2 overflow-y-auto *:border-b border-b-primary">
{posts.map((post) => (
<li
key={post.id}
className="flex items-center justify-between w-full border-b last:border-none hover:bg-gray-100"
>
<div className="flex items-center w-full gap-2">
<Receipt className="inline-block" />
{post.title}
div>
<Button
variant="destructive"
className="inline-block p-1 m-0 ml-2 h-fit"
onClick={() => deleteTodo(post.id)}
>
Delete
Button>
<Button
variant="outline"
className="inline-block p-1 m-0 ml-2 h-fit"
onClick={() =>
patchTodo({ ...post, title: `${post.title} (Updated)` })
}
>
Update
Button>
<Button
variant="secondary"
className="inline-block p-1 m-0 ml-2 h-fit"
onClick={() =>
replaceTodo({
...post,
title: `Replaced Todo`,
completed: true,
})
}
>
Replace
Button>
li>
))}
ul>
div>
);
};
export default FetchAbortControllerExample;
Explanation
AbortController: The
AbortController
is a built-in browser API that allows you to abort one or more DOM requests as and when desired.Creating an Instance: You create a new instance of
AbortController
and pass itssignal
property to the fetch request.Canceling Requests: You can cancel a request by calling
abortController.current.abort()
. This will trigger anAbortError
in the catch block of the request.Error Handling: The
handleError
function checks if the error is anAbortError
and displays an appropriate message.Cleanup: In the
useEffect
cleanup function, we abort any ongoing requests when the component unmounts.
Canceling Requests with React Query
React Query is a powerful library for managing server state in React applications. It has built-in support for canceling requests, making it easy to handle scenarios where you want to cancel a request when the component unmounts or when a new request is made.
// ReactQueryAbortControllerExample.tsx
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { LoaderIcon, Receipt } from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { useEffect, useRef } from "react";
type Todo = {
userId: number;
id: number;
title: string;
completed: boolean;
};
const fetchTodos = async (signal: AbortSignal): Promise<Todo[]> => {
const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
signal,
});
if (!res.ok) throw new Error("Failed to fetch todos");
return res.json();
};
const ReactQueryAbortControllerExample = () => {
const queryClient = useQueryClient();
const controllerRef = useRef<AbortController | null>(null);
const { toast } = useToast();
const handleError = (err: unknown) => {
let message = `[UNKNOWN_ERROR ${new Date().toISOString()}] ${JSON.stringify(
err,
Object.getOwnPropertyNames(err)
)}`;
if (err instanceof DOMException && err.name === "AbortError") {
message = `[REQUEST_ABORTED ${new Date().toISOString()}] ${err.message}`;
} else if (err instanceof Error) {
message = `[FETCH_ERROR ${new Date().toISOString()}] ${err.message}`;
}
toast({
title: "Error",
description: message,
variant: "destructive",
draggable: true,
duration: 5000,
});
};
const {
data: todos,
isFetching,
refetch,
isError,
error,
} = useQuery<Todo[]>({
queryKey: ["todos"],
queryFn: async () => {
const abortController = new AbortController();
controllerRef.current = abortController;
return fetchTodos(abortController.signal);
},
staleTime: 0,
refetchOnWindowFocus: false,
});
useEffect(() => {
if (isError) {
handleError(error);
}
return () => {
controllerRef.current?.abort();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isError, error]);
const createMutation = useMutation({
mutationFn: async (todo: Omit<Todo, "id">) => {
const abortController = new AbortController();
controllerRef.current = abortController;
const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
body: JSON.stringify(todo),
headers: { "Content-Type": "application/json" },
signal: abortController.signal,
});
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
onError: (err) => {
handleError(err);
},
});
const updateMutation = useMutation({
mutationFn: async (todo: Todo) => {
const abortController = new AbortController();
controllerRef.current = abortController;
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${todo.id}`,
{
method: "PATCH",
body: JSON.stringify(todo),
headers: { "Content-Type": "application/json" },
signal: abortController.signal,
}
);
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
onError: (err) => {
handleError(err);
},
});
const replaceMutation = useMutation({
mutationFn: async (todo: Todo) => {
const abortController = new AbortController();
controllerRef.current = abortController;
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${todo.id}`,
{
method: "PUT",
body: JSON.stringify(todo),
headers: { "Content-Type": "application/json" },
signal: abortController.signal,
}
);
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
onError: (err) => {
handleError(err);
},
});
const deleteMutation = useMutation({
mutationFn: async (id: number) => {
const abortController = new AbortController();
controllerRef.current = abortController;
await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, {
method: "DELETE",
signal: abortController.signal,
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
onError: (err) => {
handleError(err);
},
});
const isLoading =
isFetching ||
createMutation.isPending ||
updateMutation.isPending ||
replaceMutation.isPending ||
deleteMutation.isPending;
return (
<div className="flex flex-col gap-4 mt-4">
<header className="flex flex-row items-center justify-between w-full gap-4">
<div />
<Label size={"3xl"} weight={"bold"} className="text-center uppercase">
React Query AbortController Example
Label>
<Button
onClick={() =>
createMutation.mutate({
userId: 1,
title: "New Todo",
completed: false,
})
}
disabled={isLoading || createMutation.isPending}
>
Create Todo
Button>
header>
<Alert variant={"info"}>
<AlertTitle className="font-bold">Abort RequestAlertTitle>
<AlertDescription>
This example demonstrates how to cancel a fetch request with React
Query using AbortController.
AlertDescription>
Alert>
<div className="flex gap-4 *:w-full">
<Button
disabled={!isLoading}
onClick={() => controllerRef.current?.abort()}
>
Cancel Request
Button>
<Button
disabled={isLoading}
aria-label="Retry Request"
variant="outline"
onClick={() => refetch()}
>
Retry Request
Button>
div>
{isLoading && (
<div className="flex flex-col items-center gap-4">
<p className="text-center">Loading...p>
<LoaderIcon className="animate-spin text-primary" />
div>
)}
<ul className="max-h-[400px] px-2 overflow-y-auto *:border-b border-b-primary">
{todos?.map((post) => (
<li
key={post.id}
className="flex items-center justify-between w-full border-b last:border-none hover:bg-gray-100"
>
<div className="flex items-center w-full gap-2">
<Receipt className="inline-block" />
{post.title}
div>
<Button
variant="destructive"
className="p-1 ml-2 h-fit"
onClick={() => deleteMutation.mutate(post.id)}
>
Delete
Button>
<Button
variant="outline"
className="p-1 ml-2 h-fit"
onClick={() =>
updateMutation.mutate({
...post,
title: post.title + " (Updated)",
})
}
>
Update
Button>
<Button
variant="secondary"
className="p-1 ml-2 h-fit"
onClick={() =>
replaceMutation.mutate({
...post,
title: "Replaced Todo",
completed: true,
})
}
>
Replace
Button>
li>
))}
ul>
div>
);
};
export default ReactQueryAbortControllerExample;
Explanation
useQuery: The
useQuery
hook is used to fetch data. It automatically handles cancellation when the component unmounts or when a new request is made.AbortController: You create a new instance of
AbortController
and pass itssignal
property to the fetch request.Error Handling: The
handleError
function checks if the error is anAbortError
and displays an appropriate message.Mutations: The
useMutation
hook is used for creating, updating, and deleting todos. Each mutation also uses anAbortController
to allow cancellation.Cleanup: In the
useEffect
cleanup function, we abort any ongoing requests when the component unmounts.
Canceling Requests with RTK Query
RTK Query is a powerful data fetching and caching library built on top of Redux Toolkit. It provides a simple API for managing server state and has built-in support for canceling requests.
- First create a post api for the example:
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export type Post = {
userId: number;
id: number;
title: string;
completed: boolean;
};
export const postsApi = createApi({
reducerPath: "postsApi",
baseQuery: fetchBaseQuery({
baseUrl: "https://jsonplaceholder.typicode.com",
}),
tagTypes: ["Posts"],
endpoints: (builder) => ({
getPosts: builder.query<Post[], void>({
query: () => "/posts",
providesTags: ["Posts"],
}),
createPost: builder.mutation<Post, Omit<Post, "id">>({
query: (body) => ({
url: "/posts",
method: "POST",
body,
}),
invalidatesTags: ["Posts"],
}),
updatePost: builder.mutation<Post, Post>({
query: (body) => ({
url: `/posts/${body.id}`,
method: "PATCH",
body,
}),
invalidatesTags: ["Posts"],
}),
replacePost: builder.mutation<Post, Post>({
query: (body) => ({
url: `/posts/${body.id}`,
method: "PUT",
body,
}),
invalidatesTags: ["Posts"],
}),
deletePost: builder.mutation<void, number>({
query: (id) => ({
url: `/posts/${id}`,
method: "DELETE",
}),
invalidatesTags: ["Posts"],
}),
}),
});
export const {
useGetPostsQuery,
useCreatePostMutation,
useUpdatePostMutation,
useReplacePostMutation,
useDeletePostMutation,
} = postsApi;
After creating the post api, you can use the following code to implement the RTK Query example:
import { useToast } from "@/hooks/use-toast";
import {
useGetPostsQuery,
useCreatePostMutation,
useDeletePostMutation,
useReplacePostMutation,
useUpdatePostMutation,
} from "@/features/api/postApi";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Receipt, LoaderIcon } from "lucide-react";
import { useEffect, useRef } from "react";
type AbortFn = (reason?: string) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type MutationFn<TArgs> = (args: TArgs) => { abort: AbortFn } & Promise<any>;
function useAbortableMutation() {
const abortRef = useRef<AbortFn>();
async function runMutation<TArgs>(
mutationFn: MutationFn<TArgs>,
args: TArgs,
onError?: (err: unknown) => void,
onFinally?: () => void
) {
const request = mutationFn(args);
abortRef.current = request.abort;
try {
await request;
} catch (err) {
if (onError) onError(err);
} finally {
if (onFinally) onFinally();
}
}
const abort = (reason = "Aborted by user") => {
abortRef.current?.(reason);
};
return { runMutation, abort };
}
const RTKQueryExample = () => {
const { toast } = useToast();
const { runMutation, abort } = useAbortableMutation();
const { data: posts = [], isFetching, refetch, error } = useGetPostsQuery();
const [createPost, { isLoading: isCreating, error: createPostError }] =
useCreatePostMutation();
const [updatePost, { isLoading: isUpdating, error: updatePostError }] =
useUpdatePostMutation();
const [replacePost, { isLoading: isReplacing, error: replacePostError }] =
useReplacePostMutation();
const [deletePost, { isLoading: isDeleting, error: deletePostError }] =
useDeletePostMutation();
const handleError = (err: unknown) => {
let message = `[UNKNOWN_ERROR ${new Date().toISOString()}] ${JSON.stringify(
err,
Object.getOwnPropertyNames(err)
)}`;
if (err instanceof Error) {
message = `[FETCH_ERROR ${new Date().toISOString()}] ${err.message}`;
}
toast({
title: "Error",
description: message,
variant: "destructive",
draggable: true,
duration: 5000,
});
};
const handleCancel = () => {
abort("Request cancelled by user");
};
const initialError =
error ||
createPostError ||
updatePostError ||
replacePostError ||
deletePostError;
const isLoading =
isFetching || isCreating || isUpdating || isReplacing || isDeleting;
useEffect(() => {
if (initialError) {
handleError(initialError);
}
return () => {
abort("Request cancelled during cleanup");
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialError]);
return (
<div className="flex flex-col gap-4 mt-4">
<header className="flex flex-row items-center justify-between w-full gap-4">
<div />
<Label size={"3xl"} weight={"bold"} className="text-center uppercase">
RTK Query Example
Label>
<Button
onClick={async () => {
await runMutation(
createPost,
{ userId: 1, title: "New Post", completed: false },
handleError
);
}}
disabled={isLoading}
>
Create POST
Button>
header>
<Alert variant={"info"}>
<AlertTitle className="font-bold">RTK QueryAlertTitle>
<AlertDescription>
This version demonstrates CRUD with Redux Toolkit Query.
AlertDescription>
Alert>
<div className="flex gap-4 *:w-full">
<Button disabled={!isLoading} onClick={handleCancel}>
Cancel Request
Button>
<Button
disabled={isLoading}
aria-label="Retry Request"
variant="outline"
onClick={async () => {
await runMutation(refetch, undefined, (err) => {
handleError(err);
});
}}
>
Retry Request
Button>
div>
{isLoading && (
<div className="flex flex-col items-center gap-4">
<p className="text-center">Loading...p>
<LoaderIcon className="animate-spin text-primary" />
div>
)}
<ul className="max-h-[400px] px-2 overflow-y-auto *:border-b border-b-primary">
{posts.map((post) => (
<li
key={post.id}
className="flex items-center justify-between w-full border-b last:border-none hover:bg-gray-100"
>
<div className="flex items-center w-full gap-2">
<Receipt className="inline-block" />
{post.title}
div>
<Button
variant="destructive"
className="p-1 ml-2 h-fit"
onClick={async () => {
await runMutation(deletePost, post.id, handleError);
}}
>
Delete
Button>
<Button
variant="outline"
className="p-1 ml-2 h-fit"
onClick={async () =>
await runMutation(
updatePost,
{ ...post, title: post.title + " (Updated)" },
handleError
)
}
>
Update
Button>
<Button
variant="secondary"
className="p-1 ml-2 h-fit"
onClick={async () =>
await runMutation(
replacePost,
{ ...post, title: "Replaced Post", completed: true },
handleError
)
}
>
Replace
Button>
li>
))}
ul>
div>
);
};
export default RTKQueryExample;
Explanation
useGetPostsQuery: This hook is used to fetch the list of posts. It automatically handles cancellation when the component unmounts or when a new request is made.
useCreatePostMutation: This hook is used to create a new post. It returns a function that can be called to trigger the mutation.
useUpdatePostMutation: This hook is used to update an existing post. It also returns a function that can be called to trigger the mutation.
useReplacePostMutation: This hook is used to replace an existing post. It works similarly to the update mutation.
useDeletePostMutation: This hook is used to delete a post. It returns a function that can be called to trigger the mutation.
useAbortableMutation: This custom hook is used to run a mutation and handle cancellation. It provides a
runMutation
function that takes a mutation function and its arguments, and anabort
function to cancel the request.Error Handling: The
handleError
function checks if the error is anAbortError
and displays an appropriate message.Cleanup: In the
useEffect
cleanup function, we abort any ongoing requests when the component unmounts.
Conclusion
In this article, we explored how to cancel requests in React using different libraries and techniques. We covered:
- Axios: Using Cancel Tokens to cancel requests.
- Fetch API: Using AbortController to cancel requests.
- React Query: Built-in support for canceling requests with AbortController.
- RTK Query: Using Redux Toolkit Query to manage server state and cancel requests.
- Custom Hooks: Creating custom hooks to handle request cancellation.
- Error Handling: Implementing error handling for canceled requests.
- Cleanup: Ensuring that requests are canceled when the component unmounts.
- User Experience: Providing feedback to users when requests are canceled or retried.
By using these techniques, you can improve the performance and user experience of your React applications by managing network requests effectively. Whether you're using Axios, Fetch API, React Query, or RTK Query, you now have the tools to handle request cancellation in your applications.
After implementing these examples, you should have a solid understanding of how to cancel requests in React applications. You can now choose the approach that best fits your needs and integrate it into your projects.
Additional Resources
- Axios Cancel Token Documentation
- Fetch API AbortController Documentation
- React Query Documentation
- RTK Query Documentation
- React Documentation
- Redux Toolkit Documentation
- React Hooks Documentation
- JavaScript Fetch API Documentation
- JavaScript Promises Documentation
- JavaScript Async/Await Documentation
- JavaScript Error Handling Documentation
- JavaScript Fetch API Error Handling
- JavaScript Fetch API Cancellation