| name | TanStack Query Core |
| description | This skill should be used when the user asks about "TanStack Query", "React Query", "useQuery", "query keys", "staleTime", "query client setup", "query factories", "queryOptions", or needs guidance on core TanStack Query concepts, mental models, and setup patterns. |
| version | 1.0.0 |
TanStack Query Core Patterns
This skill provides guidance for working with TanStack Query (formerly React Query), covering mental models, setup, and core patterns based on best practices from TKDodo (the library maintainer).
Core Mental Model
TanStack Query is an async state manager, not a data fetching library. Understanding this distinction is fundamental:
- It manages any asynchronous state through Promises
- The actual data fetching happens in
queryFn using any tool (axios, fetch, etc.)
- It synchronizes data across the application using unique
QueryKey identifiers
Server State vs Client State
Server state differs fundamentally from client state:
- It's a snapshot in time that can become outdated
- Multiple users may modify it simultaneously
- It requires automatic synchronization to stay current
Critical rule: Use TanStack Query exclusively for async/server state. Manage client state (filters, UI toggles) separately using local state, context, or other state managers.
The Query Options API (v5+)
The recommended pattern for defining queries uses queryOptions() for type safety and reusability:
import { queryOptions, useQuery } from '@tanstack/react-query'
const todoQueryOptions = (id: string) => queryOptions({
queryKey: ['todos', id],
queryFn: () => fetchTodo(id),
staleTime: 5 * 60 * 1000,
})
const { data } = useQuery(todoQueryOptions(id))
await queryClient.prefetchQuery(todoQueryOptions(id))
Query Factories Pattern
Organize related queries using factory functions:
export const todoQueries = {
all: () => queryOptions({
queryKey: ['todos'],
queryFn: fetchAllTodos,
}),
lists: () => queryOptions({
queryKey: ['todos', 'list'],
queryFn: fetchTodoLists,
}),
detail: (id: string) => queryOptions({
queryKey: ['todos', 'detail', id],
queryFn: () => fetchTodo(id),
staleTime: 5 * 60 * 1000,
}),
}
const { data } = useQuery(todoQueries.detail(id))
queryClient.invalidateQueries({ queryKey: ['todos'] })
Understanding staleTime
staleTime is the most important configuration option. The default of 0ms means data is immediately considered stale.
How staleTime Works
- Fresh data (within staleTime): Served from cache only, no refetch
- Stale data (beyond staleTime): Served from cache, refetch in background
Configuring staleTime
queryOptions({
queryKey: ['config'],
queryFn: fetchConfig,
staleTime: Infinity,
})
queryOptions({
queryKey: ['notifications'],
queryFn: fetchNotifications,
staleTime: 30 * 1000,
})
queryOptions({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5 * 60 * 1000,
})
Global Defaults
Set sensible defaults at the QueryClient level:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
gcTime: 5 * 60 * 1000,
},
},
})
Query Keys as Dependencies
Always include query parameters in the queryKey. This ensures:
- Separate cache entries per input
- Automatic refetches when parameters change
- No stale closure bugs
- No race conditions
const todoQuery = (id: string) => queryOptions({
queryKey: ['todos', id],
queryFn: () => fetchTodo(id),
})
const todoQuery = (id: string) => queryOptions({
queryKey: ['todos'],
queryFn: () => fetchTodo(id),
})
Smart Refetch Triggers
Configure automatic refetching at strategic moments:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnMount: true,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
},
},
})
Important: Don't disable these mechanisms. Instead, adjust staleTime to control when refetches actually happen.
Common Anti-Patterns to Avoid
1. Passing Parameters to refetch()
const { refetch } = useQuery(todoQuery(id))
refetch({ id: newId })
const [todoId, setTodoId] = useState(id)
const { data } = useQuery(todoQuery(todoId))
setTodoId(newId)
2. Using for Client State
TanStack Query is not for synchronous UI state (toggles, preferences). Use Zustand, Jotai, or React state for client-only state.
3. Creating QueryClient Inside Component
function App() {
const queryClient = new QueryClient()
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}
const queryClient = new QueryClient()
function App() {
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}
Selectors with select
Use select for fine-grained subscriptions:
const productQuery = (id: string) => queryOptions({
queryKey: ['products', id],
queryFn: () => fetchProduct(id),
})
const { data: title } = useQuery({
...productQuery(id),
select: (data) => data.title,
})
For expensive transformations, stabilize with useCallback:
const { data } = useQuery({
...productQuery(id),
select: useCallback(
(data: Product) => filterByRating(data, minRating),
[minRating]
),
})
Suspense Integration (v5+)
useSuspenseQuery provides type-safe guaranteed data:
const { data } = useSuspenseQuery(todoQuery(id))
Wrap with Suspense boundary:
<Suspense fallback={<Loading />}>
<TodoDetail id={id} />
</Suspense>
Quick Reference Table
| Concept | Recommendation |
|---|
| Query definition | Use queryOptions() helper |
| Query organization | Use query factories |
| staleTime | Start with 60s, adjust per resource |
| Parameters | Always include in queryKey |
| Client state | Don't use RQ, use separate state |
| Refetch control | Adjust staleTime, don't disable refetch |
| Type safety | Let queryFn return type flow through |
Additional Resources
Reference Files
For detailed patterns and advanced techniques, consult:
references/gotchas.md - Common mistakes and FAQs from TKDodo
references/context-integration.md - Using React Query with React Context
Related Skills
- tanstack-mutations - Mutation patterns, invalidation, optimistic updates
- tanstack-types - Type safety with queryOptions and Zod
- tanstack-errors - Error handling strategies
- tanstack-forms - Forms integration patterns