tanstack-query
Manage server state in React with TanStack Query v5. Covers useMutationState, simplified optimistic updates, throwOnError, network mode (offline/PWA), and infiniteQueryOptions. Use when setting up data fetching, fixing v4→v5 migration errors (object syntax, gcTime, isPending, keepPreviousData), or debugging SSR/hydration issues with streaming server components.
What this skill does
# TanStack Query (React Query) v5 **Last Updated**: 2026-01-20 **Versions**: @tanstack/[email protected], @tanstack/[email protected] **Requires**: React 18.0+ (useSyncExternalStore), TypeScript 4.7+ (recommended) --- ## v5 New Features ### useMutationState - Cross-Component Mutation Tracking Access mutation state from anywhere without prop drilling: ```tsx import { useMutationState } from '@tanstack/react-query' function GlobalLoadingIndicator() { // Get all pending mutations const pendingMutations = useMutationState({ filters: { status: 'pending' }, select: (mutation) => mutation.state.variables, }) if (pendingMutations.length === 0) return null return <div>Saving {pendingMutations.length} items...</div> } // Filter by mutation key const todoMutations = useMutationState({ filters: { mutationKey: ['addTodo'] }, }) ``` ### Simplified Optimistic Updates New pattern using `variables` - no cache manipulation, no rollback needed: ```tsx function TodoList() { const { data: todos } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) const addTodo = useMutation({ mutationKey: ['addTodo'], mutationFn: (newTodo) => api.addTodo(newTodo), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }) }, }) // Show optimistic UI using variables from pending mutations const pendingTodos = useMutationState({ filters: { mutationKey: ['addTodo'], status: 'pending' }, select: (mutation) => mutation.state.variables, }) return ( <ul> {todos?.map(todo => <li key={todo.id}>{todo.title}</li>)} {/* Show pending items with visual indicator */} {pendingTodos.map((todo, i) => ( <li key={`pending-${i}`} style={{ opacity: 0.5 }}>{todo.title}</li> ))} </ul> ) } ``` ### throwOnError - Error Boundaries Renamed from `useErrorBoundary` (breaking change): ```tsx import { QueryErrorResetBoundary } from '@tanstack/react-query' import { ErrorBoundary } from 'react-error-boundary' function App() { return ( <QueryErrorResetBoundary> {({ reset }) => ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <div> Error! <button onClick={resetErrorBoundary}>Retry</button> </div> )}> <Todos /> </ErrorBoundary> )} </QueryErrorResetBoundary> ) } function Todos() { const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos, throwOnError: true, // ✅ v5 (was useErrorBoundary in v4) }) return <div>{data.map(...)}</div> } ``` ### Network Mode (Offline/PWA Support) Control behavior when offline: ```tsx const queryClient = new QueryClient({ defaultOptions: { queries: { networkMode: 'offlineFirst', // Use cache when offline }, }, }) // Per-query override useQuery({ queryKey: ['todos'], queryFn: fetchTodos, networkMode: 'always', // Always try, even offline (for local APIs) }) ``` | Mode | Behavior | |------|----------| | `online` (default) | Only fetch when online | | `always` | Always try (useful for local/service worker APIs) | | `offlineFirst` | Use cache first, fetch when online | **Detecting paused state:** ```tsx const { isPending, fetchStatus } = useQuery(...) // isPending + fetchStatus === 'paused' = offline, waiting for network ``` ### useQueries with Combine Combine results from parallel queries: ```tsx const results = useQueries({ queries: userIds.map(id => ({ queryKey: ['user', id], queryFn: () => fetchUser(id), })), combine: (results) => ({ data: results.map(r => r.data), pending: results.some(r => r.isPending), error: results.find(r => r.error)?.error, }), }) // Access combined result if (results.pending) return <Loading /> console.log(results.data) // [user1, user2, user3] ``` ### infiniteQueryOptions Helper Type-safe factory for infinite queries (parallel to `queryOptions`): ```tsx import { infiniteQueryOptions, useInfiniteQuery, prefetchInfiniteQuery } from '@tanstack/react-query' const todosInfiniteOptions = infiniteQueryOptions({ queryKey: ['todos', 'infinite'], queryFn: ({ pageParam }) => fetchTodosPage(pageParam), initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextCursor, }) // Reuse across hooks useInfiniteQuery(todosInfiniteOptions) useSuspenseInfiniteQuery(todosInfiniteOptions) prefetchInfiniteQuery(queryClient, todosInfiniteOptions) ``` ### maxPages - Memory Optimization Limit pages stored in cache for infinite queries: ```tsx useInfiniteQuery({ queryKey: ['posts'], queryFn: ({ pageParam }) => fetchPosts(pageParam), initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextCursor, getPreviousPageParam: (firstPage) => firstPage.prevCursor, // Required with maxPages maxPages: 3, // Only keep 3 pages in memory }) ``` **Note:** `maxPages` requires bi-directional pagination (`getNextPageParam` AND `getPreviousPageParam`). --- ## Quick Setup ```bash npm install @tanstack/react-query@latest npm install -D @tanstack/react-query-devtools@latest ``` ### Step 2: Provider + Config ```tsx // src/main.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 5, // 5 min gcTime: 1000 * 60 * 60, // 1 hour (v5: renamed from cacheTime) refetchOnWindowFocus: false, }, }, }) <QueryClientProvider client={queryClient}> <App /> <ReactQueryDevtools initialIsOpen={false} /> </QueryClientProvider> ``` ### Step 3: Query + Mutation Hooks ```tsx // src/hooks/useTodos.ts import { useQuery, useMutation, useQueryClient, queryOptions } from '@tanstack/react-query' // Query options factory (v5 pattern) export const todosQueryOptions = queryOptions({ queryKey: ['todos'], queryFn: async () => { const res = await fetch('/api/todos') if (!res.ok) throw new Error('Failed to fetch') return res.json() }, }) export function useTodos() { return useQuery(todosQueryOptions) } export function useAddTodo() { const queryClient = useQueryClient() return useMutation({ mutationFn: async (newTodo) => { const res = await fetch('/api/todos', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(newTodo), }) if (!res.ok) throw new Error('Failed to add') return res.json() }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }) }, }) } // Usage: function TodoList() { const { data, isPending, isError, error } = useTodos() const { mutate } = useAddTodo() if (isPending) return <div>Loading...</div> if (isError) return <div>Error: {error.message}</div> return <ul>{data.map(todo => <li key={todo.id}>{todo.title}</li>)}</ul> } ``` --- ## Critical Rules ### Always Do ✅ **Use object syntax for all hooks** ```tsx // v5 ONLY supports this: useQuery({ queryKey, queryFn, ...options }) useMutation({ mutationFn, ...options }) ``` ✅ **Use array query keys** ```tsx queryKey: ['todos'] // List queryKey: ['todos', id] // Detail queryKey: ['todos', { filter }] // Filtered ``` ✅ **Configure staleTime appropriately** ```tsx staleTime: 1000 * 60 * 5 // 5 min - prevents excessive refetches ``` ✅ **Use isPending for initial loading state** ```tsx if (isPending) return <Loading /> // isPending = no data yet AND fetching ``` ✅ **Throw errors in queryFn** ```tsx if (!response.ok) throw new Error('Failed') ``` ✅ **Invalidate queries after mutations** ```tsx onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }) } ``` ✅ **Use queryOptions factory for reusable patterns** ```tsx const opts = queryOptions({ queryKey, queryFn }) useQuery(opts) useSuspenseQuery(opts) prefetchQuery(opts) ``` ✅ **Use gcTime (not cacheTime)** ```tsx gc
Related in Web Dev
generating-lwc-components
IncludedLightning Web Components with PICKLES methodology and 165-point scoring. Use this skill when the user creates or edits LWC components, builds wire service patterns, or writes Jest tests for LWC. TRIGGER when: user creates/edits LWC components, touches lwc/**/*.js, .html, .css, .js-meta.xml files, or asks about wire service, SLDS, or Jest LWC tests. DO NOT TRIGGER when: Apex classes (use generating-apex), Aura components, or Visualforce.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Set up queries with useQuery, mutations with useMutation, configure QueryClient caching strategies, implement optimistic updates, and handle infinite scroll with useInfiniteQuery. Use when: setting up data fetching in React projects, migrating from v4 to v5, or fixing object syntax required errors, query callbacks removed issues, cacheTime renamed to gcTime, isPending vs isLoading confusion, keepPreviousData removed problems.
document-processor-api
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
nutrient-document-processing
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
accelint-nextjs-best-practices
IncludedNext.js performance optimization and best practices. Use when writing Next.js code (App Router or Pages Router); implementing Server Components, Server Actions, or API routes; optimizing RSC serialization, data fetching, or server-side rendering; reviewing Next.js code for performance issues; fixing authentication in Server Actions; or implementing Suspense boundaries, parallel data fetching, or request deduplication.
clerk-auth
IncludedClerk auth with API version 2025-11-10 breaking changes (billing endpoints, payment_source→payment_method), Next.js v6 async auth(), PKCE for custom OAuth, credential stuffing defense. Use when: troubleshooting "Missing Clerk Secret Key", JWKS errors, authorizedParties CSRF, JWT size limits (1.2KB), 431 header errors (Vite dev mode), or testing with 424242 OTP.