Setup
import { useQuery } from '@tanstack/react-query'
export function useTodo(todoId: string) {
return useQuery({
queryKey: ['todo', todoId],
queryFn: async ({ signal }) => {
const response = await fetch(`/api/todos/${todoId}`, { signal })
return response.json() as Promise<{ id: string; title: string }>
},
})
}
Core Patterns
Cancel before optimistic writes
import { QueryClient } from '@tanstack/react-query'
const queryClient = new QueryClient()
export async function prepareTodoWrite() {
await queryClient.cancelQueries({ queryKey: ['todos'] })
}
Consume one signal across nested fetches
import { queryOptions } from '@tanstack/react-query'
export const todosWithDetailsOptions = queryOptions({
queryKey: ['todos-with-details'],
queryFn: async ({ signal }) => {
const todos = await fetch('/api/todos', { signal }).then(
(response) => response.json() as Promise<Array<{ id: string }>>,
)
return Promise.all(
todos.map((todo) =>
fetch(`/api/todos/${todo.id}`, { signal }).then((response) =>
response.json(),
),
),
)
},
})
Cancel by query key
import { QueryClient } from '@tanstack/react-query'
const queryClient = new QueryClient()
export function cancelTodos() {
return queryClient.cancelQueries({ queryKey: ['todos'] })
}
Common Mistakes
HIGH Ignoring AbortSignal
Wrong:
import { useQuery } from '@tanstack/react-query'
export function useTodo(id: string) {
return useQuery({
queryKey: ['todo', id],
queryFn: async () => fetch(`/api/todos/${id}`).then((r) => r.json()),
})
}
Correct:
import { useQuery } from '@tanstack/react-query'
export function useTodo(id: string) {
return useQuery({
queryKey: ['todo', id],
queryFn: async ({ signal }) =>
fetch(`/api/todos/${id}`, { signal }).then((r) => r.json()),
})
}
TanStack Query provides an AbortSignal; the request is only cancelled if the query function consumes it.
Source: TanStack/query:docs/framework/react/guides/query-cancellation.md
HIGH Assuming unmount cancels
Wrong:
import { useQuery } from '@tanstack/react-query'
export function useReport() {
return useQuery({
queryKey: ['report'],
queryFn: async () => fetch('/api/report').then((r) => r.json()),
})
}
Correct:
import { useQuery } from '@tanstack/react-query'
export function useReport() {
return useQuery({
queryKey: ['report'],
queryFn: async ({ signal }) =>
fetch('/api/report', { signal }).then((r) => r.json()),
})
}
Unused queries can continue and populate cache unless the signal is consumed.
Source: TanStack/query:docs/framework/react/guides/query-cancellation.md
HIGH Suspense cancellation expected
Wrong:
import { useSuspenseQuery } from '@tanstack/react-query'
export function useTodo(id: string) {
return useSuspenseQuery({
queryKey: ['todo', id],
queryFn: async ({ signal }) =>
fetch(`/api/todos/${id}`, { signal }).then((r) => r.json()),
})
}
Correct:
import { useQuery } from '@tanstack/react-query'
export function useTodo(id: string) {
return useQuery({
queryKey: ['todo', id],
queryFn: async ({ signal }) =>
fetch(`/api/todos/${id}`, { signal }).then((r) => r.json()),
})
}
Cancellation limitations apply to Suspense hooks; use non-suspense queries when cancellation behavior is required.
Source: TanStack/query:docs/framework/react/guides/query-cancellation.md