Capítulo 31 de 80

Chapter 31: Query Cancellation

Core Idea

Every queryFn receives an AbortSignal; consuming it (wiring it into fetch/axios/etc.) makes that query genuinely cancellable — un-consumed signals just leave unmounted/inactive queries running to completion in the background (harmless, since the result still populates the cache for later reuse).

Key Concepts

  • Default behavior without consuming signal: an unmounted or now-inactive query's in-flight Promise is not cancelled — it finishes and its result lands in the cache, ready if the component remounts before GC. This is a feature, not a bug: no wasted work if the user comes right back.
  • Consuming the signal changes that: once signal is passed into the underlying request (fetch(url, { signal }), axios.get(url, { signal }), etc.), the query becomes genuinely abortable, and cancelling reverts its state to what it was before the fetch started.
  • Per-client wiring: fetch/modern axios (v0.22+) accept signal directly; older axios needs a CancelToken bridged via signal.addEventListener('abort', ...); XMLHttpRequest needs the same abort-listener bridge calling .abort(); graphql-request accepts signal on request() (v4+) or the client constructor (pre-v4).
  • Manual cancellation: queryClient.cancelQueries({ queryKey }) cancels a query on demand (e.g. from a "Cancel" button) — reverts state, and if signal is consumed, actually aborts the underlying request too.
  • Cancel options: { silent: true } suppresses CancelledError propagation to onError/observers, returning the retry promise instead of rejecting; { revert: true } (default) restores the pre-fetch data/status and resets fetchStatus to idle — only throwing if there was no prior data to revert to.
  • Suspense limitation: cancellation does not work with useSuspenseQuery/useSuspenseQueries/useSuspenseInfiniteQuery.

Code Examples

// fetch — pass signal straight through
useQuery({
  queryKey: ['todos'],
  queryFn: async ({ signal }) => {
    const res = await fetch('/todos', { signal })
    return res.json()
  },
})

// Manual cancel, e.g. from a Cancel button
const queryClient = useQueryClient()
<button onClick={() => queryClient.cancelQueries({ queryKey: ['todos'] })}>Cancel</button>
  • What it demonstrates: wiring signal into fetch to make the query genuinely abortable, and triggering that cancellation manually via cancelQueries.

Key Takeaways

  1. If a query doesn't need real cancellation (cheap/fast requests), it's fine to leave signal unconsumed — the default "let it finish, cache it anyway" behavior is intentional, not a gap.
  2. Wire signal through whenever a request is expensive/slow enough that an abandoned navigation should actually stop it server-side, not just client-side.
  3. Cancellation silently does nothing under Suspense query hooks — don't rely on cancelQueries there.

Connects To

  • Query Functions: signal arrives via the same QueryFunctionContext covered there.
  • Optimistic Updates: cancelQueries is a required first step before an optimistic setQueryData write.