| name | tanstack-query |
| description | TanStack Query v5 (React Query) reference for data fetching, caching,
and server state management in React. Use when: (1) writing useQuery,
useMutation, or useInfiniteQuery hooks, (2) setting up QueryClient
and queryOptions, (3) implementing optimistic updates or cache
invalidation, (4) configuring SSR/hydration with Next.js App Router
or Pages Router, (5) testing React Query hooks, (6) working with
TypeScript types, Suspense, or advanced patterns like dependent
queries and infinite scroll.
|
TanStack Query v5 (React)
<quick_reference>
Setup
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
gcTime: 5 * 60 * 1000,
retry: 3,
refetchOnWindowFocus: true,
},
},
})
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
)
}
Important Defaults
| Default | Value | Notes |
|---|
staleTime | 0 | Cached data is immediately stale; triggers background refetch on mount/focus/reconnect |
gcTime | 5 min | Inactive queries garbage collected after 5 minutes |
retry | 3 (queries) / 0 (mutations) | Queries retry 3x with exponential backoff; mutations do NOT retry |
refetchOnWindowFocus | true | Stale queries refetch when tab regains focus |
refetchOnReconnect | true | Stale queries refetch when network reconnects |
refetchOnMount | true | Stale queries refetch when new instance mounts |
structuralSharing | true | Preserves referential identity if data is structurally equal |
Key recommendation: Set staleTime above 0 to control refetch frequency rather than disabling individual refetch triggers.
</quick_reference>
queryOptions — co-locate key + fn
Always use queryOptions to define query configurations. It enables type inference across useQuery, prefetchQuery, getQueryData, and setQueryData.
import { queryOptions, infiniteQueryOptions } from '@tanstack/react-query'
export function todosOptions(filters: TodoFilters) {
return queryOptions({
queryKey: ['todos', filters],
queryFn: () => fetchTodos(filters),
staleTime: 5 * 1000,
})
}
useQuery(todosOptions({ status: 'done' }))
useSuspenseQuery(todosOptions({ status: 'done' }))
queryClient.prefetchQuery(todosOptions({ status: 'done' }))
queryClient.setQueryData(todosOptions({ status: 'done' }).queryKey, newData)
const cached = queryClient.getQueryData(todosOptions({ status: 'done' }).queryKey)
For infinite queries, use infiniteQueryOptions (same pattern, adds initialPageParam and getNextPageParam).
useQuery
const {
data,
error,
status,
isPending,
isError,
isSuccess,
isFetching,
isLoading,
isPlaceholderData,
isStale,
refetch,
fetchStatus,
} = useQuery({
queryKey: ['todos', userId],
queryFn: () => fetchTodos(userId),
enabled: !!userId,
staleTime: 60_000,
select: (data) => data.filter(t => !t.done),
placeholderData: keepPreviousData,
})
Query states: status tells you "do we have data?"; fetchStatus tells you "is the queryFn running?". They are orthogonal — a query can be pending + paused (no data, no network).
Query Keys
Keys must be Arrays. They are hashed deterministically.
useQuery({ queryKey: ['todos', { status, page }] })
useQuery({ queryKey: ['todos', { page, status }] })
useQuery({ queryKey: ['todos', status, page] })
useQuery({ queryKey: ['todos', page, status] })
Rule: If your queryFn depends on a variable, include it in the queryKey. The key acts as a dependency array.
useMutation
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (newTodo: CreateTodoInput) => api.post('/todos', newTodo),
onSuccess: (data, variables) => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
queryClient.setQueryData(['todos', data.id], data)
},
onError: (error, variables, onMutateResult) => {},
onSettled: (data, error, variables, onMutateResult) => {},
})
mutation.mutate({ title: 'New todo' })
mutation.mutate(input, { onSuccess: () => navigate('/todos') })
const data = await mutation.mutateAsync(input)
Lifecycle: onMutate → mutationFn → onSuccess/onError → onSettled. Callbacks returning promises are awaited.
Gotcha: Per-call mutate() callbacks only fire for the latest call if mutations overlap. Use useMutation-level callbacks for reliable logic.
Query Invalidation
queryClient.invalidateQueries({ queryKey: ['todos'] })
queryClient.invalidateQueries({ queryKey: ['todos'], exact: true })
queryClient.invalidateQueries({
predicate: (query) => query.queryKey[0] === 'todos' && query.queryKey[1]?.version >= 10,
})
queryClient.invalidateQueries()
Invalidation marks queries as stale and triggers background refetch for active (rendered) queries.
Optimistic Updates
Approach 1: Via the UI (simpler, recommended)
Render optimistic state from variables directly in JSX:
const { mutate, variables, isPending, isError } = useMutation({
mutationFn: (text: string) => api.post('/todos', { text }),
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})
{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
{isPending && <li style={{ opacity: 0.5 }}>{variables}</li>}
Access pending mutations from other components with useMutationState:
const pendingTodos = useMutationState<string>({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => mutation.state.variables,
})
Approach 2: Via cache (with rollback)
useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo, context) => {
await context.client.cancelQueries({ queryKey: ['todos'] })
const previous = context.client.getQueryData(['todos'])
context.client.setQueryData(['todos'], (old) => [...old, newTodo])
return { previous }
},
onError: (err, newTodo, onMutateResult, context) => {
context.client.setQueryData(['todos'], onMutateResult.previous)
},
onSettled: (data, error, variables, onMutateResult, context) => {
context.client.invalidateQueries({ queryKey: ['todos'] })
},
})
Always cancelQueries before optimistic update to prevent background refetches from overwriting.
Infinite Queries
const {
data,
fetchNextPage,
fetchPreviousPage,
hasNextPage,
hasPreviousPage,
isFetchingNextPage,
isFetchingPreviousPage,
} = useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam }) => fetchProjects(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages) => lastPage.nextCursor ?? undefined,
maxPages: 3,
})
{data.pages.map((page, i) => (
<Fragment key={i}>
{page.items.map(item => <div key={item.id}>{item.name}</div>)}
</Fragment>
))}
<button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage}>
{isFetchingNextPage ? 'Loading...' : hasNextPage ? 'Load More' : 'No more'}
</button>
Gotcha: data is { pages, pageParams }, not flat data. initialData and placeholderData must match this shape.
Paginated Queries (keep previous data)
import { keepPreviousData, useQuery } from '@tanstack/react-query'
const { data, isPlaceholderData } = useQuery({
queryKey: ['projects', page],
queryFn: () => fetchProjects(page),
placeholderData: keepPreviousData,
})
queryClient.prefetchQuery({
queryKey: ['projects', page + 1],
queryFn: () => fetchProjects(page + 1),
})
Prefetching
const prefetch = () => queryClient.prefetchQuery(todosOptions())
<button onMouseEnter={prefetch} onFocus={prefetch} onClick={handleClick}>Show</button>
function Layout({ id }: { id: string }) {
usePrefetchQuery(commentsOptions(id))
return (
<Suspense fallback="Loading...">
<Article id={id} />
</Suspense>
)
}
queryClient.prefetchInfiniteQuery({
...projectsInfiniteOptions(),
pages: 3,
})
Dependent Queries
const { data: user } = useQuery({
queryKey: ['user', email],
queryFn: () => getUserByEmail(email),
})
const { data: projects } = useQuery({
queryKey: ['projects', user?.id],
queryFn: () => getProjectsByUser(user!.id),
enabled: !!user?.id,
})
Type-safe disabling with skipToken:
import { skipToken } from '@tanstack/react-query'
const { data } = useQuery({
queryKey: ['projects', userId],
queryFn: userId ? () => getProjects(userId) : skipToken,
})
skipToken prevents refetch() from working — use enabled: false if you need manual refetch.
setQueryData — immutability
queryClient.setQueryData(['todo', id], (old) => {
if (old) old.title = 'new'
return old
})
queryClient.setQueryData(['todo', id], (old) =>
old ? { ...old, title: 'new' } : old
)
Further Reference
- Full API signatures (useQuery, useMutation, useInfiniteQuery, QueryClient): See references/api-reference.md
- SSR & Next.js (hydration, App Router, streaming): See references/ssr-nextjs.md
- Testing (renderHook, mocking, setup): See references/testing.md
- Advanced patterns (TypeScript, Suspense, waterfalls, network modes): See references/advanced-patterns.md