Use when adding new error messages to React, or seeing "unknown error code" warnings.
npx skills add umxr/react-query-skills --skill "react-query"
Install specific skill from multi-skill repository
# Description
TanStack Query (React Query) best practices and patterns from TkDodo's authoritative guides. Use when writing, reviewing, or debugging React Query code including queries, mutations, caching, optimistic updates, and TypeScript integration. Triggers on useQuery, useMutation, useInfiniteQuery, QueryClient, queryKey, staleTime, cacheTime, gcTime, invalidateQueries, or React Query patterns.
# SKILL.md
name: react-query
description: TanStack Query (React Query) best practices and patterns from TkDodo's authoritative guides. Use when writing, reviewing, or debugging React Query code including queries, mutations, caching, optimistic updates, and TypeScript integration. Triggers on useQuery, useMutation, useInfiniteQuery, QueryClient, queryKey, staleTime, cacheTime, gcTime, invalidateQueries, or React Query patterns.
license: MIT
metadata:
author: community
version: "1.0.0"
source: "TkDodo's Blog (tkdodo.eu)"
TanStack Query (React Query) Best Practices
Comprehensive guide for TanStack Query based on TkDodo's authoritative blog posts. TkDodo (Dominik Dorfmeister) is a core maintainer of TanStack Query. Contains patterns across 17 categories covering queries, mutations, caching, TypeScript, testing, and advanced patterns.
When to Apply
Reference these guidelines when:
- Writing new useQuery, useMutation, or useInfiniteQuery hooks
- Structuring query keys for a feature or application
- Implementing optimistic updates or cache synchronization
- Debugging unexpected refetches or stale data issues
- Integrating React Query with TypeScript
- Testing components that use React Query
- Deciding between React Query and other solutions
Rule Categories by Priority
| Priority | Category | Impact | Key Concept |
|---|---|---|---|
| 1 | Mental Model | CRITICAL | React Query is an async state manager, not a fetching library |
| 2 | Query Keys | CRITICAL | Keys are dependencies - include all variables that affect data |
| 3 | Status Handling | HIGH | Check data first, then error, then loading |
| 4 | Mutations | HIGH | Invalidation vs direct cache updates, optimistic patterns |
| 5 | TypeScript | HIGH | Prefer inference over explicit generics |
| 6 | Cache Management | MEDIUM-HIGH | placeholderData vs initialData, seeding strategies |
| 7 | Error Handling | MEDIUM-HIGH | Global callbacks, Error Boundaries, granular throwOnError |
| 8 | Render Optimization | MEDIUM | Tracked queries, structural sharing, select |
| 9 | Testing | MEDIUM | Fresh QueryClient per test, disable retries, use MSW |
| 10 | Advanced Patterns | LOW-MEDIUM | WebSockets, Router integration, Infinite queries |
Quick Reference
1. Core Mental Model (CRITICAL)
mental-not-fetching-lib- React Query is an async state manager, not a fetching librarymental-server-vs-client- Server state is borrowed; client state is ownedmental-stale-time- staleTime controls when data becomes eligible for background refetchmental-gc-time- gcTime controls when inactive cache entries are garbage collectedmental-no-sync-to-state- Never copy query data to local state with useState/useEffect
2. Query Keys (CRITICAL)
keys-array-format- Always use array keys:['todos']not'todos'keys-generic-to-specific- Structure from most generic to most specifickeys-include-dependencies- Include all variables that determine what data to fetchkeys-factory-pattern- Use query key factories for consistency and type safetykeys-exact-match- Keys must match exactly:['item', '1']!==['item', 1]
3. Status Handling (HIGH)
status-data-first- Check data availability before error statestatus-avoid-status-first- Don't hide cached data during background refetch errorsstatus-fetch-status- Use fetchStatus for paused/fetching states separate from data status
4. Mutations (HIGH)
mutation-invalidation- Prefer invalidation over direct cache updates for safetymutation-return-promise- Return invalidation promises to keep mutations in loading statemutation-prefer-mutate- Prefer mutate() over mutateAsync() to avoid manual error handlingmutation-single-arg- Mutations accept one variable - use objects for multiple valuesmutation-callback-lifecycle- Query logic in useMutation callbacks; UI actions in mutate() callbacksmutation-optimistic-when- Use optimistic updates for high-confidence toggles, not navigation
5. TypeScript (HIGH)
ts-prefer-inference- Let TypeScript infer from well-typed queryFn return typests-no-destructure- Keep query object intact for proper type narrowingts-query-options- Use queryOptions() helper for type-safe reusable query definitionsts-factories-with-options- Combine key factories with queryOptions for full type safety
6. Cache Management (MEDIUM-HIGH)
cache-placeholder-vs-initial- placeholderData for fake data; initialData for real cached datacache-initial-data-updated-at- Specify initialDataUpdatedAt for proper stale time calculationcache-seed-pull- Pull from list cache to detail cache with initialDatacache-seed-push- Push to detail caches after fetching lists with setQueryDatacache-prefetch- Use prefetchQuery in loaders or on hover for instant navigation
7. Error Handling (MEDIUM-HIGH)
error-boundaries- Use throwOnError with Error Boundaries for critical errorserror-granular-throw- Use function throwOnError for selective error boundary routingerror-global-callbacks- Use QueryCache onError for background refetch toastserror-fetch-api- Check response.ok with fetch API - it doesn't reject on 4xx/5xxerror-rethrow- Always re-throw errors after logging in catch blocks
8. Render Optimization (MEDIUM)
render-tracked-queries- Use tracked queries (default v4+) for automatic optimizationrender-select- Use select option for computed data and partial subscriptionsrender-structural-sharing- Leverage structural sharing; disable for large datasetsrender-no-spread- Avoid spreading query result to preserve tracked query benefits
9. Testing (MEDIUM)
test-fresh-client- Create new QueryClient for each test for isolationtest-disable-retries- Set retry: false in test configurationtest-use-msw- Use Mock Service Worker for network mockingtest-wait-for- Use waitFor with expect() for async assertions
10. Advanced Patterns (LOW-MEDIUM)
advanced-websocket-invalidate- Use WebSocket events to invalidateQueriesadvanced-ws-stale-time- Set high staleTime when WebSockets handle updatesadvanced-router-loader- Combine router loaders with React Query cachingadvanced-infinite-page-param- Use getNextPageParam returning null for end of listadvanced-context-provider- Use Context to guarantee data availability without undefinedadvanced-suspense-query- Use useSuspenseQuery for guaranteed data in components
Detailed Rules
mental-not-fetching-lib
React Query is agnostic about how you fetch. It only needs a Promise that resolves or rejects. Handle baseURLs, headers, and GraphQL in your data layer.
mental-server-vs-client
Server state is a snapshot you don't own - other users can modify it. Client state (dark mode, UI toggles) is synchronous and yours. Treating them the same leads to problems.
keys-factory-pattern
const todoKeys = {
all: ['todos'] as const,
lists: () => [...todoKeys.all, 'list'] as const,
list: (filters: Filters) => [...todoKeys.lists(), { filters }] as const,
details: () => [...todoKeys.all, 'detail'] as const,
detail: (id: number) => [...todoKeys.details(), id] as const,
}
status-data-first
// Correct: prioritize cached data
if (query.data) return <Content data={query.data} />
if (query.error) return <Error error={query.error} />
return <Loading />
mutation-return-promise
// Correct: mutation stays loading during invalidation
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] })
// Wrong: mutation completes before invalidation
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }) }
ts-query-options
const todosQuery = queryOptions({
queryKey: ['todos'],
queryFn: fetchTodos,
})
// Reusable and type-safe
useQuery(todosQuery)
queryClient.prefetchQuery(todosQuery)
const data = queryClient.getQueryData(todosQuery.queryKey) // Typed!
cache-placeholder-vs-initial
- placeholderData: Observer-level, never cached, always refetches, use for "fake" data
- initialData: Cache-level, persisted, respects staleTime, use for "real" data from other cache entries
error-global-callbacks
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
// Only toast for background refetch failures (has existing data)
if (query.state.data !== undefined) {
toast.error(`Background update failed: ${error.message}`)
}
},
}),
})
advanced-router-loader
// Loader
export const loader = (queryClient: QueryClient) =>
async ({ params }: LoaderFunctionArgs) => {
const query = todoQuery(params.id!)
return queryClient.getQueryData(query.queryKey) ??
await queryClient.fetchQuery(query)
}
// Component - instant data from loader, background updates from React Query
const { data } = useQuery({ ...todoQuery(id), initialData: useLoaderData() })
Anti-Patterns to Avoid
Don't sync to local state
// Anti-pattern
const { data } = useQuery({...})
const [localData, setLocalData] = useState(data)
useEffect(() => { setLocalData(data) }, [data])
// Correct: use data directly, or use select for transformations
const { data } = useQuery({...})
Don't use QueryCache as state manager
setQueryData is for optimistic updates and mutation responses only. Background refetches will override manually-set data.
Don't create unstable QueryClient
// Wrong: recreates on every render
function App() {
const queryClient = new QueryClient()
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}
// Correct: stable reference
const queryClient = new QueryClient()
function App() {
return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}
When NOT to Use React Query
- React Server Components: Use framework-native data fetching
- Next.js/Remix with simple needs: Built-in solutions may suffice
- GraphQL with normalized cache needs: Consider Apollo Client or urql
- No background refetch requirements: Static SSR may be enough
Resources
- Official docs: https://tanstack.com/query
- TkDodo's blog: https://tkdodo.eu/blog (authoritative source for best practices)
- Key articles: Practical React Query, Thinking in React Query, Effective React Query Keys
For complete explanations and code examples, see references/react-query-context-source.md
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.