Query cancellation allows you to abort in-flight requests when they’re no longer needed. This prevents race conditions, reduces unnecessary network usage, and improves application performance.
Automatic Cancellation
TanStack Query automatically cancels queries in several scenarios:
- When a query becomes inactive (all observers unsubscribe)
- When a new request starts for the same query key
- When the component unmounts
- When
queryClient.cancelQueries() is called
import { useQuery } from '@tanstack/react-query'
function User({ userId }) {
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: async ({ signal }) => {
const response = await fetch(`/api/users/${userId}`, { signal })
return await response.json()
},
})
return <div>{data?.name}</div>
}
The query function receives an AbortSignal in the context. Pass this signal to your fetch call to enable automatic cancellation.
Query Function Context
The query function receives a context object with an AbortSignal:
interface QueryFunctionContext {
queryKey: QueryKey
signal: AbortSignal
meta: QueryMeta | undefined
pageParam?: unknown // For infinite queries
}
Use the signal to cancel requests:
const { data } = useQuery({
queryKey: ['todos'],
queryFn: async ({ signal }) => {
const response = await fetch('/api/todos', { signal })
if (!response.ok) {
throw new Error('Failed to fetch todos')
}
return await response.json()
},
})
Axios Integration
Cancel Axios requests using the signal:
import axios from 'axios'
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: async ({ signal }) => {
const { data } = await axios.get(`/api/users/${userId}`, {
signal,
})
return data
},
})
Modern versions of Axios (>= 0.22.0) support the signal option natively. For older versions, use a CancelToken.
Custom Cancellation Logic
Implement custom cleanup when a query is cancelled:
const { data } = useQuery({
queryKey: ['data'],
queryFn: async ({ signal }) => {
const controller = new AbortController()
// Link the query signal to your controller
signal.addEventListener('abort', () => {
controller.abort()
})
try {
const response = await fetch('/api/data', {
signal: controller.signal,
})
return await response.json()
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request was cancelled')
}
throw error
}
},
})
Manual Query Cancellation
Cancel queries manually using queryClient.cancelQueries():
import { useQueryClient } from '@tanstack/react-query'
function SearchComponent() {
const queryClient = useQueryClient()
const [searchTerm, setSearchTerm] = useState('')
const { data } = useQuery({
queryKey: ['search', searchTerm],
queryFn: async ({ signal }) => {
const response = await fetch(
`/api/search?q=${searchTerm}`,
{ signal }
)
return await response.json()
},
enabled: searchTerm.length > 2,
})
const handleClear = () => {
setSearchTerm('')
// Cancel any in-flight search queries
queryClient.cancelQueries({ queryKey: ['search'] })
}
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<button onClick={handleClear}>Clear</button>
{data && <SearchResults results={data} />}
</div>
)
}
Import useQueryClient
Get access to the query client instance.
Call cancelQueries
Use query filters to cancel specific queries.
Handle cancellation
The query function’s signal will be aborted, triggering cleanup.
Preventing Race Conditions
Cancellation prevents race conditions when query keys change:
function UserProfile({ userId }) {
const { data, isFetching } = useQuery({
queryKey: ['user', userId],
queryFn: async ({ signal }) => {
// Simulate slow network
await new Promise((resolve) => setTimeout(resolve, 2000))
const response = await fetch(`/api/users/${userId}`, { signal })
return await response.json()
},
})
// When userId changes, the previous request is cancelled
// This ensures we always show data for the current userId
return <div>{data?.name}</div>
}
Without cancellation, a slow request for user “1” could complete after a fast request for user “2”, causing the UI to show incorrect data.
Cancellation with Infinite Queries
Infinite queries support cancellation for all pages:
const { data, fetchNextPage } = useInfiniteQuery({
queryKey: ['projects'],
queryFn: async ({ pageParam, signal }) => {
const response = await fetch(
`/api/projects?cursor=${pageParam}`,
{ signal }
)
return await response.json()
},
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
Cancelling Before Mutations
Cancel queries before performing mutations to avoid conflicts:
const updateUser = useMutation({
mutationFn: async (user) => {
const response = await fetch(`/api/users/${user.id}`, {
method: 'PUT',
body: JSON.stringify(user),
})
return await response.json()
},
onMutate: async (newUser) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['user', newUser.id] })
// Snapshot the previous value
const previousUser = queryClient.getQueryData(['user', newUser.id])
// Optimistically update
queryClient.setQueryData(['user', newUser.id], newUser)
return { previousUser }
},
onError: (err, newUser, context) => {
// Rollback on error
queryClient.setQueryData(
['user', newUser.id],
context.previousUser
)
},
})
Cancelling queries before optimistic updates ensures that in-flight refetches don’t overwrite your optimistic data.
Handling Cancellation Errors
Detect when a request was cancelled:
const { data, error } = useQuery({
queryKey: ['data'],
queryFn: async ({ signal }) => {
try {
const response = await fetch('/api/data', { signal })
return await response.json()
} catch (err) {
if (err.name === 'AbortError') {
// Request was cancelled, don't show error to user
console.log('Request cancelled')
throw err
}
// Other errors should be handled normally
throw err
}
},
})
Custom Abort Controllers
Create your own abort controller for complex scenarios:
const { data } = useQuery({
queryKey: ['complex-data'],
queryFn: async ({ signal }) => {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 5000)
signal.addEventListener('abort', () => {
controller.abort()
clearTimeout(timeout)
})
try {
const response = await fetch('/api/data', {
signal: controller.signal,
})
clearTimeout(timeout)
return await response.json()
} catch (error) {
clearTimeout(timeout)
throw error
}
},
})
Combine TanStack Query’s signal with your own timeout logic for requests that should fail after a certain duration.
WebSocket Cancellation
Cancel WebSocket connections when queries are cancelled:
const { data } = useQuery({
queryKey: ['live-data'],
queryFn: ({ signal }) => {
return new Promise((resolve, reject) => {
const ws = new WebSocket('wss://api.example.com/live')
signal.addEventListener('abort', () => {
ws.close()
reject(new Error('Connection cancelled'))
})
ws.onmessage = (event) => {
resolve(JSON.parse(event.data))
}
ws.onerror = (error) => {
reject(error)
}
})
},
})
GraphQL Query Cancellation
Cancel GraphQL queries with Apollo Client or other libraries:
import { useQuery } from '@tanstack/react-query'
import { gql } from 'graphql-request'
const { data } = useQuery({
queryKey: ['user', userId],
queryFn: async ({ signal }) => {
const controller = new AbortController()
signal.addEventListener('abort', () => controller.abort())
const response = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`,
variables: { id: userId },
}),
signal: controller.signal,
})
const { data, errors } = await response.json()
if (errors) throw new Error(errors[0].message)
return data.user
},
})
Retry and Cancellation
Cancellation works alongside retry logic:
const { data } = useQuery({
queryKey: ['data'],
queryFn: async ({ signal }) => {
const response = await fetch('/api/data', { signal })
if (!response.ok) {
throw new Error('Failed to fetch')
}
return await response.json()
},
retry: 3,
retryDelay: 1000,
})
If a query is cancelled during a retry, all pending retries are also cancelled. The query won’t continue retrying after cancellation.
Testing Query Cancellation
Test cancellation behavior in your components:
import { render, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
test('cancels query on unmount', async () => {
const queryClient = new QueryClient()
let aborted = false
const { unmount } = render(
<QueryClientProvider client={queryClient}>
<Component />
</QueryClientProvider>
)
// Unmount to trigger cancellation
unmount()
await waitFor(() => {
expect(aborted).toBe(true)
})
})