Skip to main content
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>
  )
}
1

Import useQueryClient

Get access to the query client instance.
2

Call cancelQueries

Use query filters to cancel specific queries.
3

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)
  })
})

Build docs developers (and LLMs) love