KS
Killer-Skills

tanstack-query — how to use tanstack-query how to use tanstack-query, tanstack-query vs react-query, tanstack-query install, react query v5 setup guide, tanstack-query alternative, what is tanstack-query, tanstack-query react 18, tanstack-query typescript, tanstack-query devtools, tanstack-query mutation tracking

v1.0.0
GitHub

About this Skill

Essential for React-based AI Agents requiring optimized data fetching and state synchronization across components. tanstack-query is a JavaScript library for React that provides data fetching, caching, and mutation management capabilities, also known as React Query v5.

Features

Provides useMutationState for cross-component mutation tracking
Supports React 18.0+ with useSyncExternalStore
Requires TypeScript 4.7+ for optimal performance
Includes @tanstack/react-query-devtools for debugging and testing
Enables access to mutation state from anywhere without prop drilling
Offers version 5.90.19 of @tanstack/react-query for improved stability

# Core Topics

HuynhSang2005 HuynhSang2005
[0]
[0]
Updated: 3/6/2026

Quality Score

Top 5%
39
Excellent
Based on code quality & docs
Installation
SYS Universal Install (Auto-Detect)
Cursor IDE Windsurf IDE VS Code IDE
> npx killer-skills add HuynhSang2005/blog-personal-nextjs/tanstack-query

Agent Capability Analysis

The tanstack-query MCP Server by HuynhSang2005 is an open-source Categories.community integration for Claude and other AI agents, enabling seamless task automation and capability expansion. Optimized for how to use tanstack-query, tanstack-query vs react-query, tanstack-query install.

Ideal Agent Persona

Essential for React-based AI Agents requiring optimized data fetching and state synchronization across components.

Core Value

Enables real-time mutation state tracking with useMutationState and provides efficient caching mechanisms through React Query's stale-while-revalidate pattern. Integrates seamlessly with React 18's useSyncExternalStore for consistent state management.

Capabilities Granted for tanstack-query MCP Server

Synchronizing mutation states across distributed agent components
Optimizing data fetching strategies for real-time applications
Debugging API call patterns with React Query DevTools integration

! Prerequisites & Limits

  • Requires React 18.0+ for useSyncExternalStore compatibility
  • TypeScript 4.7+ recommended for full type safety
  • Limited to React ecosystem implementations
Project
SKILL.md
30.1 KB
.cursorrules
1.2 KB
package.json
240 B
Ready
UTF-8

# Tags

[No tags]
SKILL.md
Readonly

TanStack Query (React Query) v5

Last Updated: 2026-01-20 Versions: @tanstack/react-query@5.90.19, @tanstack/react-query-devtools@5.91.2 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
1import { useMutationState } from '@tanstack/react-query' 2 3function GlobalLoadingIndicator() { 4 // Get all pending mutations 5 const pendingMutations = useMutationState({ 6 filters: { status: 'pending' }, 7 select: (mutation) => mutation.state.variables, 8 }) 9 10 if (pendingMutations.length === 0) return null 11 return <div>Saving {pendingMutations.length} items...</div> 12} 13 14// Filter by mutation key 15const todoMutations = useMutationState({ 16 filters: { mutationKey: ['addTodo'] }, 17})

Simplified Optimistic Updates

New pattern using variables - no cache manipulation, no rollback needed:

tsx
1function TodoList() { 2 const { data: todos } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) 3 4 const addTodo = useMutation({ 5 mutationKey: ['addTodo'], 6 mutationFn: (newTodo) => api.addTodo(newTodo), 7 onSuccess: () => { 8 queryClient.invalidateQueries({ queryKey: ['todos'] }) 9 }, 10 }) 11 12 // Show optimistic UI using variables from pending mutations 13 const pendingTodos = useMutationState({ 14 filters: { mutationKey: ['addTodo'], status: 'pending' }, 15 select: (mutation) => mutation.state.variables, 16 }) 17 18 return ( 19 <ul> 20 {todos?.map(todo => <li key={todo.id}>{todo.title}</li>)} 21 {/* Show pending items with visual indicator */} 22 {pendingTodos.map((todo, i) => ( 23 <li key={`pending-${i}`} style={{ opacity: 0.5 }}>{todo.title}</li> 24 ))} 25 </ul> 26 ) 27}

throwOnError - Error Boundaries

Renamed from useErrorBoundary (breaking change):

tsx
1import { QueryErrorResetBoundary } from '@tanstack/react-query' 2import { ErrorBoundary } from 'react-error-boundary' 3 4function App() { 5 return ( 6 <QueryErrorResetBoundary> 7 {({ reset }) => ( 8 <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( 9 <div> 10 Error! <button onClick={resetErrorBoundary}>Retry</button> 11 </div> 12 )}> 13 <Todos /> 14 </ErrorBoundary> 15 )} 16 </QueryErrorResetBoundary> 17 ) 18} 19 20function Todos() { 21 const { data } = useQuery({ 22 queryKey: ['todos'], 23 queryFn: fetchTodos, 24 throwOnError: true, // ✅ v5 (was useErrorBoundary in v4) 25 }) 26 return <div>{data.map(...)}</div> 27}

Network Mode (Offline/PWA Support)

Control behavior when offline:

tsx
1const queryClient = new QueryClient({ 2 defaultOptions: { 3 queries: { 4 networkMode: 'offlineFirst', // Use cache when offline 5 }, 6 }, 7}) 8 9// Per-query override 10useQuery({ 11 queryKey: ['todos'], 12 queryFn: fetchTodos, 13 networkMode: 'always', // Always try, even offline (for local APIs) 14})
ModeBehavior
online (default)Only fetch when online
alwaysAlways try (useful for local/service worker APIs)
offlineFirstUse cache first, fetch when online

Detecting paused state:

tsx
1const { isPending, fetchStatus } = useQuery(...) 2// isPending + fetchStatus === 'paused' = offline, waiting for network

useQueries with Combine

Combine results from parallel queries:

tsx
1const results = useQueries({ 2 queries: userIds.map(id => ({ 3 queryKey: ['user', id], 4 queryFn: () => fetchUser(id), 5 })), 6 combine: (results) => ({ 7 data: results.map(r => r.data), 8 pending: results.some(r => r.isPending), 9 error: results.find(r => r.error)?.error, 10 }), 11}) 12 13// Access combined result 14if (results.pending) return <Loading /> 15console.log(results.data) // [user1, user2, user3]

infiniteQueryOptions Helper

Type-safe factory for infinite queries (parallel to queryOptions):

tsx
1import { infiniteQueryOptions, useInfiniteQuery, prefetchInfiniteQuery } from '@tanstack/react-query' 2 3const todosInfiniteOptions = infiniteQueryOptions({ 4 queryKey: ['todos', 'infinite'], 5 queryFn: ({ pageParam }) => fetchTodosPage(pageParam), 6 initialPageParam: 0, 7 getNextPageParam: (lastPage) => lastPage.nextCursor, 8}) 9 10// Reuse across hooks 11useInfiniteQuery(todosInfiniteOptions) 12useSuspenseInfiniteQuery(todosInfiniteOptions) 13prefetchInfiniteQuery(queryClient, todosInfiniteOptions)

maxPages - Memory Optimization

Limit pages stored in cache for infinite queries:

tsx
1useInfiniteQuery({ 2 queryKey: ['posts'], 3 queryFn: ({ pageParam }) => fetchPosts(pageParam), 4 initialPageParam: 0, 5 getNextPageParam: (lastPage) => lastPage.nextCursor, 6 getPreviousPageParam: (firstPage) => firstPage.prevCursor, // Required with maxPages 7 maxPages: 3, // Only keep 3 pages in memory 8})

Note: maxPages requires bi-directional pagination (getNextPageParam AND getPreviousPageParam).


Quick Setup

bash
1npm install @tanstack/react-query@latest 2npm install -D @tanstack/react-query-devtools@latest

Step 2: Provider + Config

tsx
1// src/main.tsx 2import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 3import { ReactQueryDevtools } from '@tanstack/react-query-devtools' 4 5const queryClient = new QueryClient({ 6 defaultOptions: { 7 queries: { 8 staleTime: 1000 * 60 * 5, // 5 min 9 gcTime: 1000 * 60 * 60, // 1 hour (v5: renamed from cacheTime) 10 refetchOnWindowFocus: false, 11 }, 12 }, 13}) 14 15<QueryClientProvider client={queryClient}> 16 <App /> 17 <ReactQueryDevtools initialIsOpen={false} /> 18</QueryClientProvider>

Step 3: Query + Mutation Hooks

tsx
1// src/hooks/useTodos.ts 2import { useQuery, useMutation, useQueryClient, queryOptions } from '@tanstack/react-query' 3 4// Query options factory (v5 pattern) 5export const todosQueryOptions = queryOptions({ 6 queryKey: ['todos'], 7 queryFn: async () => { 8 const res = await fetch('/api/todos') 9 if (!res.ok) throw new Error('Failed to fetch') 10 return res.json() 11 }, 12}) 13 14export function useTodos() { 15 return useQuery(todosQueryOptions) 16} 17 18export function useAddTodo() { 19 const queryClient = useQueryClient() 20 return useMutation({ 21 mutationFn: async (newTodo) => { 22 const res = await fetch('/api/todos', { 23 method: 'POST', 24 headers: { 'Content-Type': 'application/json' }, 25 body: JSON.stringify(newTodo), 26 }) 27 if (!res.ok) throw new Error('Failed to add') 28 return res.json() 29 }, 30 onSuccess: () => { 31 queryClient.invalidateQueries({ queryKey: ['todos'] }) 32 }, 33 }) 34} 35 36// Usage: 37function TodoList() { 38 const { data, isPending, isError, error } = useTodos() 39 const { mutate } = useAddTodo() 40 41 if (isPending) return <div>Loading...</div> 42 if (isError) return <div>Error: {error.message}</div> 43 return <ul>{data.map(todo => <li key={todo.id}>{todo.title}</li>)}</ul> 44}

Critical Rules

Always Do

Use object syntax for all hooks

tsx
1// v5 ONLY supports this: 2useQuery({ queryKey, queryFn, ...options }) 3useMutation({ mutationFn, ...options })

Use array query keys

tsx
1queryKey: ['todos'] // List 2queryKey: ['todos', id] // Detail 3queryKey: ['todos', { filter }] // Filtered

Configure staleTime appropriately

tsx
1staleTime: 1000 * 60 * 5 // 5 min - prevents excessive refetches

Use isPending for initial loading state

tsx
1if (isPending) return <Loading /> 2// isPending = no data yet AND fetching

Throw errors in queryFn

tsx
1if (!response.ok) throw new Error('Failed')

Invalidate queries after mutations

tsx
1onSuccess: () => { 2 queryClient.invalidateQueries({ queryKey: ['todos'] }) 3}

Use queryOptions factory for reusable patterns

tsx
1const opts = queryOptions({ queryKey, queryFn }) 2useQuery(opts) 3useSuspenseQuery(opts) 4prefetchQuery(opts)

Use gcTime (not cacheTime)

tsx
1gcTime: 1000 * 60 * 60 // 1 hour

Never Do

Never use v4 array/function syntax

tsx
1// v4 (removed in v5): 2useQuery(['todos'], fetchTodos, options) // ❌ 3 4// v5 (correct): 5useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) // ✅

Never use query callbacks (onSuccess, onError, onSettled in queries)

tsx
1// v5 removed these from queries: 2useQuery({ 3 queryKey: ['todos'], 4 queryFn: fetchTodos, 5 onSuccess: (data) => {}, // ❌ Removed in v5 6}) 7 8// Use useEffect instead: 9const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) 10useEffect(() => { 11 if (data) { 12 // Do something 13 } 14}, [data]) 15 16// Or use mutation callbacks (still supported): 17useMutation({ 18 mutationFn: addTodo, 19 onSuccess: () => {}, // ✅ Still works for mutations 20})

Never use deprecated options

tsx
1// Deprecated in v5: 2cacheTime: 1000 // ❌ Use gcTime instead 3isLoading: true // ❌ Meaning changed, use isPending 4keepPreviousData: true // ❌ Use placeholderData instead 5onSuccess: () => {} // ❌ Removed from queries 6useErrorBoundary: true // ❌ Use throwOnError instead

Never assume isLoading means "no data yet"

tsx
1// v5 changed this: 2isLoading = isPending && isFetching // ❌ Now means "pending AND fetching" 3isPending = no data yet // ✅ Use this for initial load

Never forget initialPageParam for infinite queries

tsx
1// v5 requires this: 2useInfiniteQuery({ 3 queryKey: ['projects'], 4 queryFn: ({ pageParam }) => fetchProjects(pageParam), 5 initialPageParam: 0, // ✅ Required in v5 6 getNextPageParam: (lastPage) => lastPage.nextCursor, 7})

Never use enabled with useSuspenseQuery

tsx
1// Not allowed: 2useSuspenseQuery({ 3 queryKey: ['todo', id], 4 queryFn: () => fetchTodo(id), 5 enabled: !!id, // ❌ Not available with suspense 6}) 7 8// Use conditional rendering instead: 9{id && <TodoComponent id={id} />}

Never rely on refetchOnMount: false for errored queries

tsx
1// Doesn't work - errors are always stale 2useQuery({ 3 queryKey: ['data'], 4 queryFn: failingFetch, 5 refetchOnMount: false, // ❌ Ignored when query has error 6}) 7 8// Use retryOnMount instead 9useQuery({ 10 queryKey: ['data'], 11 queryFn: failingFetch, 12 refetchOnMount: false, 13 retryOnMount: false, // ✅ Prevents refetch for errored queries 14 retry: 0, 15})

Known Issues Prevention

This skill prevents 16 documented issues from v5 migration, SSR/hydration bugs, and common mistakes:

Issue #1: Object Syntax Required

Error: useQuery is not a function or type errors Source: v5 Migration Guide Why It Happens: v5 removed all function overloads, only object syntax works Prevention: Always use useQuery({ queryKey, queryFn, ...options })

Before (v4):

tsx
1useQuery(['todos'], fetchTodos, { staleTime: 5000 })

After (v5):

tsx
1useQuery({ 2 queryKey: ['todos'], 3 queryFn: fetchTodos, 4 staleTime: 5000 5})

Issue #2: Query Callbacks Removed

Error: Callbacks don't run, TypeScript errors Source: v5 Breaking Changes Why It Happens: onSuccess, onError, onSettled removed from queries (still work in mutations) Prevention: Use useEffect for side effects, or move logic to mutation callbacks

Before (v4):

tsx
1useQuery({ 2 queryKey: ['todos'], 3 queryFn: fetchTodos, 4 onSuccess: (data) => { 5 console.log('Todos loaded:', data) 6 }, 7})

After (v5):

tsx
1const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) 2useEffect(() => { 3 if (data) { 4 console.log('Todos loaded:', data) 5 } 6}, [data])

Issue #3: Status Loading → Pending

Error: UI shows wrong loading state Source: v5 Migration: isLoading renamed Why It Happens: status: 'loading' renamed to status: 'pending', isLoading meaning changed Prevention: Use isPending for initial load, isLoading for "pending AND fetching"

Before (v4):

tsx
1const { data, isLoading } = useQuery(...) 2if (isLoading) return <div>Loading...</div>

After (v5):

tsx
1const { data, isPending, isLoading } = useQuery(...) 2if (isPending) return <div>Loading...</div> 3// isLoading = isPending && isFetching (fetching for first time)

Issue #4: cacheTime → gcTime

Error: cacheTime is not a valid option Source: v5 Migration: gcTime Why It Happens: Renamed to better reflect "garbage collection time" Prevention: Use gcTime instead of cacheTime

Before (v4):

tsx
1useQuery({ 2 queryKey: ['todos'], 3 queryFn: fetchTodos, 4 cacheTime: 1000 * 60 * 60, 5})

After (v5):

tsx
1useQuery({ 2 queryKey: ['todos'], 3 queryFn: fetchTodos, 4 gcTime: 1000 * 60 * 60, 5})

Issue #5: useSuspenseQuery + enabled

Error: Type error, enabled option not available Source: GitHub Discussion #6206 Why It Happens: Suspense guarantees data is available, can't conditionally disable Prevention: Use conditional rendering instead of enabled option

Before (v4/incorrect):

tsx
1useSuspenseQuery({ 2 queryKey: ['todo', id], 3 queryFn: () => fetchTodo(id), 4 enabled: !!id, // ❌ Not allowed 5})

After (v5/correct):

tsx
1// Conditional rendering: 2{id ? ( 3 <TodoComponent id={id} /> 4) : ( 5 <div>No ID selected</div> 6)} 7 8// Inside TodoComponent: 9function TodoComponent({ id }: { id: number }) { 10 const { data } = useSuspenseQuery({ 11 queryKey: ['todo', id], 12 queryFn: () => fetchTodo(id), 13 // No enabled option needed 14 }) 15 return <div>{data.title}</div> 16}

Issue #6: initialPageParam Required

Error: initialPageParam is required type error Source: v5 Migration: Infinite Queries Why It Happens: v4 passed undefined as first pageParam, v5 requires explicit value Prevention: Always specify initialPageParam for infinite queries

Before (v4):

tsx
1useInfiniteQuery({ 2 queryKey: ['projects'], 3 queryFn: ({ pageParam = 0 }) => fetchProjects(pageParam), 4 getNextPageParam: (lastPage) => lastPage.nextCursor, 5})

After (v5):

tsx
1useInfiniteQuery({ 2 queryKey: ['projects'], 3 queryFn: ({ pageParam }) => fetchProjects(pageParam), 4 initialPageParam: 0, // ✅ Required 5 getNextPageParam: (lastPage) => lastPage.nextCursor, 6})

Issue #7: keepPreviousData Removed

Error: keepPreviousData is not a valid option Source: v5 Migration: placeholderData Why It Happens: Replaced with more flexible placeholderData function Prevention: Use placeholderData: keepPreviousData helper

Before (v4):

tsx
1useQuery({ 2 queryKey: ['todos', page], 3 queryFn: () => fetchTodos(page), 4 keepPreviousData: true, 5})

After (v5):

tsx
1import { keepPreviousData } from '@tanstack/react-query' 2 3useQuery({ 4 queryKey: ['todos', page], 5 queryFn: () => fetchTodos(page), 6 placeholderData: keepPreviousData, 7})

Issue #8: TypeScript Error Type Default

Error: Type errors with error handling Source: v5 Migration: Error Types Why It Happens: v4 used unknown, v5 defaults to Error type Prevention: If throwing non-Error types, specify error type explicitly

Before (v4 - error was unknown):

tsx
1const { error } = useQuery({ 2 queryKey: ['data'], 3 queryFn: async () => { 4 if (Math.random() > 0.5) throw 'custom error string' 5 return data 6 }, 7}) 8// error: unknown

After (v5 - specify custom error type):

tsx
1const { error } = useQuery<DataType, string>({ 2 queryKey: ['data'], 3 queryFn: async () => { 4 if (Math.random() > 0.5) throw 'custom error string' 5 return data 6 }, 7}) 8// error: string | null 9 10// Or better: always throw Error objects 11const { error } = useQuery({ 12 queryKey: ['data'], 13 queryFn: async () => { 14 if (Math.random() > 0.5) throw new Error('custom error') 15 return data 16 }, 17}) 18// error: Error | null (default)

Issue #9: Streaming Server Components Hydration Error

Error: Hydration failed because the initial UI does not match what was rendered on the server Source: GitHub Issue #9642 Affects: v5.82.0+ with streaming SSR (void prefetch pattern) Why It Happens: Race condition where hydrate() resolves synchronously but query.fetch() creates async retryer, causing isFetching/isStale mismatch between server and client Prevention: Don't conditionally render based on fetchStatus with useSuspenseQuery and streaming prefetch, OR await prefetch instead of void pattern

Before (causes hydration error):

tsx
1// Server: void prefetch 2streamingQueryClient.prefetchQuery({ queryKey: ['data'], queryFn: getData }); 3 4// Client: conditional render on fetchStatus 5const { data, isFetching } = useSuspenseQuery({ queryKey: ['data'], queryFn: getData }); 6return <>{data && <div>{data}</div>} {isFetching && <Loading />}</>;

After (workaround):

tsx
1// Option 1: Await prefetch 2await streamingQueryClient.prefetchQuery({ queryKey: ['data'], queryFn: getData }); 3 4// Option 2: Don't render based on fetchStatus with Suspense 5const { data } = useSuspenseQuery({ queryKey: ['data'], queryFn: getData }); 6return <div>{data}</div>; // No conditional on isFetching

Status: Known issue, being investigated by maintainers. Requires implementation of getServerSnapshot in useSyncExternalStore.

Issue #10: useQuery Hydration Error with Prefetching

Error: Text content mismatch during hydration Source: GitHub Issue #9399 Affects: v5.x with server-side prefetching Why It Happens: tryResolveSync detects resolved promises in RSC payload and extracts data synchronously during hydration, bypassing normal pending state Prevention: Use useSuspenseQuery instead of useQuery for SSR, or avoid conditional rendering based on isLoading

Before (causes hydration error):

tsx
1// Server Component 2const queryClient = getServerQueryClient(); 3await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos }); 4 5// Client Component 6function Todos() { 7 const { data, isLoading } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos }); 8 if (isLoading) return <div>Loading...</div>; // Server renders this 9 return <div>{data.length} todos</div>; // Client hydrates with this 10}

After (workaround):

tsx
1// Use useSuspenseQuery instead 2function Todos() { 3 const { data } = useSuspenseQuery({ queryKey: ['todos'], queryFn: fetchTodos }); 4 return <div>{data.length} todos</div>; 5}

Status: "At the top of my OSS list of things to fix" - maintainer Ephem (Nov 2025). Requires implementing getServerSnapshot in useSyncExternalStore.

Issue #11: refetchOnMount Not Respected for Errored Queries

Error: Queries refetch on mount despite refetchOnMount: false Source: GitHub Issue #10018 Affects: v5.90.16+ Why It Happens: Errored queries with no data are always treated as stale. This is intentional to avoid permanently showing error states Prevention: Use retryOnMount: false instead of (or in addition to) refetchOnMount: false

Before (refetches despite setting):

tsx
1const { data, error } = useQuery({ 2 queryKey: ['data'], 3 queryFn: () => { throw new Error('Fails') }, 4 refetchOnMount: false, // Ignored when query is in error state 5 retry: 0, 6}); 7// Query refetches every time component mounts

After (correct):

tsx
1const { data, error } = useQuery({ 2 queryKey: ['data'], 3 queryFn: failingFetch, 4 refetchOnMount: false, 5 retryOnMount: false, // ✅ Prevents refetch on mount for errored queries 6 retry: 0, 7});

Status: Documented behavior (intentional). The name retryOnMount is slightly misleading - it controls whether errored queries trigger a new fetch on mount, not automatic retries.

Issue #12: Mutation Callback Signature Breaking Change (v5.89.0)

Error: TypeScript errors in mutation callbacks Source: GitHub Issue #9660 Affects: v5.89.0+ Why It Happens: onMutateResult parameter added between variables and context, changing callback signatures from 3 params to 4 Prevention: Update all mutation callbacks to accept 4 parameters instead of 3

Before (v5.88 and earlier):

tsx
1useMutation({ 2 mutationFn: addTodo, 3 onError: (error, variables, context) => { 4 // context is now onMutateResult, missing final context param 5 }, 6 onSuccess: (data, variables, context) => { 7 // Same issue 8 } 9});

After (v5.89.0+):

tsx
1useMutation({ 2 mutationFn: addTodo, 3 onError: (error, variables, onMutateResult, context) => { 4 // onMutateResult = return value from onMutate 5 // context = mutation function context 6 }, 7 onSuccess: (data, variables, onMutateResult, context) => { 8 // Correct signature with 4 parameters 9 } 10});

Note: If you don't use onMutate, the onMutateResult parameter will be undefined. This breaking change was introduced in a patch version.

Issue #13: Readonly Query Keys Break Partial Matching (v5.90.8)

Error: Type 'readonly ["todos", string]' is not assignable to type '["todos", string]' Source: GitHub Issue #9871 | Fixed in PR #9872 Affects: v5.90.8 only (fixed in v5.90.9) Why It Happens: Partial query matching broke TypeScript types for readonly query keys (using as const) Prevention: Upgrade to v5.90.9+ or use type assertions if stuck on v5.90.8

Before (v5.90.8 - TypeScript error):

tsx
1export function todoQueryKey(id?: string) { 2 return id ? ['todos', id] as const : ['todos'] as const; 3} 4// Type: readonly ['todos', string] | readonly ['todos'] 5 6useMutation({ 7 mutationFn: addTodo, 8 onSuccess: () => { 9 queryClient.invalidateQueries({ 10 queryKey: todoQueryKey('123') 11 // Error: readonly ['todos', string] not assignable to ['todos', string] 12 }); 13 } 14});

After (v5.90.9+):

tsx
1// Works correctly with readonly types 2queryClient.invalidateQueries({ 3 queryKey: todoQueryKey('123') // ✅ No type error 4});

Status: Fixed in v5.90.9. Particularly affected users of code generators like openapi-react-query that produce readonly query keys.

Issue #14: useMutationState Type Inference Lost

Error: mutation.state.variables typed as unknown instead of actual type Source: GitHub Issue #9825 Affects: All v5.x versions Why It Happens: Fuzzy mutation key matching prevents guaranteed type inference (same issue as queryClient.getQueryCache().find()) Prevention: Explicitly cast types in the select callback

Before (type inference doesn't work):

tsx
1const addTodo = useMutation({ 2 mutationKey: ['addTodo'], 3 mutationFn: (todo: Todo) => api.addTodo(todo), 4}); 5 6const pendingTodos = useMutationState({ 7 filters: { mutationKey: ['addTodo'], status: 'pending' }, 8 select: (mutation) => { 9 return mutation.state.variables; // Type: unknown 10 }, 11});

After (with explicit cast):

tsx
1const pendingTodos = useMutationState({ 2 filters: { mutationKey: ['addTodo'], status: 'pending' }, 3 select: (mutation) => mutation.state.variables as Todo, 4}); 5// Or cast the entire state: 6select: (mutation) => mutation.state as MutationState<Todo, Error, Todo, unknown>

Status: Known limitation of fuzzy matching. No planned fix.

Issue #15: Query Cancellation in StrictMode with fetchQuery

Error: CancelledError when using fetchQuery() with useQuery Source: GitHub Issue #9798 Affects: Development only (React StrictMode) Why It Happens: StrictMode causes double mount/unmount. When useQuery unmounts and is the last observer, it cancels the query even if fetchQuery() is also running Prevention: This is expected development-only behavior. Doesn't affect production

Example:

tsx
1async function loadData() { 2 try { 3 const data = await queryClient.fetchQuery({ 4 queryKey: ['data'], 5 queryFn: fetchData, 6 }); 7 console.log('Loaded:', data); // Never logs in StrictMode 8 } catch (error) { 9 console.error('Failed:', error); // CancelledError 10 } 11} 12 13function Component() { 14 const { data } = useQuery({ queryKey: ['data'], queryFn: fetchData }); 15 // In StrictMode, component unmounts/remounts, cancelling fetchQuery 16}

Workaround:

tsx
1// Keep query observed with staleTime 2const { data } = useQuery({ 3 queryKey: ['data'], 4 queryFn: fetchData, 5 staleTime: Infinity, // Keeps query active 6});

Status: Expected StrictMode behavior, not a bug. Production builds are unaffected.

Issue #16: invalidateQueries Only Refetches Active Queries

Error: Inactive queries not refetching despite invalidateQueries() call Source: GitHub Issue #9531 Affects: All v5.x versions Why It Happens: Documentation was misleading - invalidateQueries() only refetches "active" queries by default, not "all" queries Prevention: Use refetchType: 'all' to force refetch of inactive queries

Default behavior:

tsx
1// Only active queries (currently being observed) will refetch 2queryClient.invalidateQueries({ queryKey: ['todos'] });

To refetch inactive queries:

tsx
1queryClient.invalidateQueries({ 2 queryKey: ['todos'], 3 refetchType: 'all' // Refetch active AND inactive 4});

Status: Documentation fixed to clarify "active" queries. This is the intended behavior.


Community Tips

Note: These tips come from community experts and maintainer blogs. Verify against your version.

Tip: Query Options with Multiple Listeners

Source: TkDodo's Blog - API Design Lessons | Confidence: HIGH Applies to: v5.27.3+

When multiple components use the same query with different options (like staleTime), the "last write wins" rule applies for future fetches, but the current in-flight query uses its original options. This can cause unexpected behavior when components mount at different times.

Example of unexpected behavior:

tsx
1// Component A mounts first 2function ComponentA() { 3 const { data } = useQuery({ 4 queryKey: ['todos'], 5 queryFn: fetchTodos, 6 staleTime: 5000, // Applied initially 7 }); 8} 9 10// Component B mounts while A's query is in-flight 11function ComponentB() { 12 const { data } = useQuery({ 13 queryKey: ['todos'], 14 queryFn: fetchTodos, 15 staleTime: 60000, // Won't affect current fetch, only future ones 16 }); 17}

Recommended approach:

tsx
1// Write options as functions that reference latest values 2const getStaleTime = () => shouldUseLongCache ? 60000 : 5000; 3 4useQuery({ 5 queryKey: ['todos'], 6 queryFn: fetchTodos, 7 staleTime: getStaleTime(), // Evaluated on each render 8});

Tip: refetch() is NOT for Changed Parameters

Source: Avoiding Common Mistakes with TanStack Query | Confidence: HIGH

The refetch() function should ONLY be used for refreshing with the same parameters (like a manual "reload" button). For new parameters (filters, page numbers, search terms, etc.), include them in the query key instead.

Anti-pattern:

tsx
1// ❌ Wrong - using refetch() for different parameters 2const [page, setPage] = useState(1); 3const { data, refetch } = useQuery({ 4 queryKey: ['todos'], // Same key for all pages 5 queryFn: () => fetchTodos(page), 6}); 7 8// This refetches with OLD page value, not new one 9<button onClick={() => { setPage(2); refetch(); }}>Next</button>

Correct pattern:

tsx
1// ✅ Correct - include parameters in query key 2const [page, setPage] = useState(1); 3const { data } = useQuery({ 4 queryKey: ['todos', page], // Key changes with page 5 queryFn: () => fetchTodos(page), 6 // Query automatically refetches when page changes 7}); 8 9<button onClick={() => setPage(2)}>Next</button> // Just update state

When to use refetch():

tsx
1// ✅ Manual refresh of same data (refresh button) 2const { data, refetch } = useQuery({ 3 queryKey: ['todos'], 4 queryFn: fetchTodos, 5}); 6 7<button onClick={() => refetch()}>Refresh</button> // Same parameters

Key Patterns

Dependent Queries (Query B waits for Query A):

tsx
1const { data: posts } = useQuery({ 2 queryKey: ['users', userId, 'posts'], 3 queryFn: () => fetchUserPosts(userId), 4 enabled: !!user, // Wait for user 5})

Parallel Queries (fetch multiple at once):

tsx
1const results = useQueries({ 2 queries: ids.map(id => ({ queryKey: ['todos', id], queryFn: () => fetchTodo(id) })), 3})

Prefetching (preload on hover):

tsx
1queryClient.prefetchQuery({ queryKey: ['todo', id], queryFn: () => fetchTodo(id) })

Infinite Scroll (useInfiniteQuery):

tsx
1useInfiniteQuery({ 2 queryKey: ['todos', 'infinite'], 3 queryFn: ({ pageParam }) => fetchTodosPage(pageParam), 4 initialPageParam: 0, // Required in v5 5 getNextPageParam: (lastPage) => lastPage.nextCursor, 6})

Query Cancellation (auto-cancel on queryKey change):

tsx
1queryFn: async ({ signal }) => { 2 const res = await fetch(`/api/todos?q=${search}`, { signal }) 3 return res.json() 4}

Data Transformation (select):

tsx
1select: (data) => data.filter(todo => todo.completed)

Avoid Request Waterfalls: Fetch in parallel when possible (don't chain queries unless truly dependent)


Official Docs: https://tanstack.com/query/latest | v5 Migration: https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5 | GitHub: https://github.com/TanStack/query | Context7: /websites/tanstack_query

Related Skills

Looking for an alternative to tanstack-query or building a Categories.community AI Agent? Explore these related open-source MCP Servers.

View All

widget-generator

Logo of f
f

widget-generator is an open-source AI agent skill for creating widget plugins that are injected into prompt feeds on prompts.chat. It supports two rendering modes: standard prompt widgets using default PromptCard styling and custom render widgets built as full React components.

149.6k
0
Design

chat-sdk

Logo of lobehub
lobehub

chat-sdk is a unified TypeScript SDK for building chat bots across multiple platforms, providing a single interface for deploying bot logic.

73.0k
0
Communication

zustand

Logo of lobehub
lobehub

The ultimate space for work and life — to find, build, and collaborate with agent teammates that grow with you. We are taking agent harness to the next level — enabling multi-agent collaboration, effortless agent team design, and introducing agents as the unit of work interaction.

72.8k
0
Communication

data-fetching

Logo of lobehub
lobehub

The ultimate space for work and life — to find, build, and collaborate with agent teammates that grow with you. We are taking agent harness to the next level — enabling multi-agent collaboration, effortless agent team design, and introducing agents as the unit of work interaction.

72.8k
0
Communication