Back to React & React Native

tanstack-query

tanstack queryreact queryreactdata fetchingstate managementasynchronousfrontendjavascripttypescript
⭐ 860πŸ“„ MITπŸ•’ 2026-06-11Source β†—

Install this skill

npx skills add jezweb/claude-skills

Works across Claude Code, Cursor, Codex, Copilot & Antigravity

TanStack Query v5 serves as a data-fetching and state-management engine for React applications. It manages asynchronous operations by handling caching, background updates, and stale data synchronization directly within the component lifecycle. The library centralizes remote state, removing the need for manual useEffect boilerplate or complex Redux stores for server-bound data. With v5, it introduces improved type safety via factory patterns, refined error handling through standard error boundaries, and explicit network mode controls for offline environments. By separating the logic of fetching, caching, and state transitions, the library allows components to remain declarative while reacting precisely to remote data changes. It effectively abstracts the complexity of loading states, error handling, and mutation lifecycle tracking, ensuring the UI remains performant even when dealing with intermittent connectivity or high-frequency data updates.

When to Use This Skill

  • β€’Synchronizing server-side REST or GraphQL API data with React components
  • β€’Building responsive user interfaces that feel instant via optimistic updates
  • β€’Managing complex paginated feeds with infinite scroll requirements
  • β€’Handling intermittent network scenarios in PWA or mobile environments
  • β€’Coordinating parallel data fetching across multiple dashboard widgets

How to Invoke This Skill

Example prompts that trigger this skill in Claude Code, Cursor, or Antigravity:

  • β€œHow do I cache my API responses in React?
  • β€œShow me how to handle optimistic UI updates with TanStack Query
  • β€œHow to manage infinite scroll with page caching limits?
  • β€œSetup TanStack Query v5 with custom network modes
  • β€œFilter mutations globally across different components

Pro Tips

  • πŸ’‘When requesting code, specify the exact TanStack Query version (e.g., 'v5') and React version for optimal output.
  • πŸ’‘Ask the agent to explain complex caching scenarios or query invalidation logic using this skill to deepen your understanding.
  • πŸ’‘Combine with a 'TypeScript' skill to ensure strongly-typed TanStack Query implementations are generated.

What this skill does

  • β€’Declarative management of remote server state with automated caching
  • β€’Global tracking of mutation states without manual prop passing
  • β€’Optimistic UI updates synchronized with mutation variables
  • β€’Advanced infinite scrolling logic with memory-efficient page capping
  • β€’Granular network behavior control including offline-first caching strategies
  • β€’Type-safe query configuration using factory functions

When not to use it

  • βœ•Managing strictly local, non-persistent UI state like form inputs or theme toggles
  • βœ•Small applications where simple fetch calls inside useEffect are sufficient
  • βœ•Scenarios requiring high-performance WebSocket streams that need custom lifecycle management

Example workflow

  1. Initialize the QueryClient provider at the root of the React tree
  2. Define query logic using the infiniteQueryOptions factory for consistent typing
  3. Implement the useInfiniteQuery hook with maxPages set to manage memory
  4. Configure mutation logic for POST/PUT requests with onSuccess invalidation
  5. Use useMutationState to display pending status in global loading UI
  6. Wrap components in QueryErrorResetBoundary for consistent error recovery

Prerequisites

  • –React 18.0 or higher
  • –TypeScript 4.7 or higher
  • –Functional knowledge of React hooks

Pitfalls & limitations

  • !Forgetting to implement bi-directional pagination when using maxPages
  • !Misconfiguring networkMode leading to unexpected behavior on local dev servers
  • !Over-using global state for data that should remain scoped to specific components

FAQ

What happened to useErrorBoundary?
In v5, it was renamed to throwOnError for consistency across the API surface.
Can I use this for non-server state?
While possible, it is optimized for server-state; managing purely local UI state is better handled by standard React context or state hooks.
What is the difference between offlineFirst and always network modes?
offlineFirst pulls from cache when disconnected, while always forces a fetch attempt regardless of connection status, which is useful for local-only APIs.
Why does maxPages require bi-directional pagination?
The cache needs both getNextPageParam and getPreviousPageParam to intelligently prune and maintain the correct data window in memory.

How it compares

Unlike manual useEffect fetching which is prone to race conditions and inconsistent caching, TanStack Query provides an integrated, battle-tested architecture that automatically handles refetching and state invalidation.

Source & trust

⭐ 860 starsπŸ“„ MITπŸ•’ Updated 2026-06-11
πŸ“„ Full skill instructions β€” original source: jezweb/claude-skills
# 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:

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:

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):

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:

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:**
const { isPending, fetchStatus } = useQuery(...)
// isPending + fetchStatus === 'paused' = offline, waiting for network


### useQueries with Combine

Combine results from parallel queries:

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):

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:

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

npm install @tanstack/react-query@latest
npm install -D @tanstack/react-query-devtools@latest


### Step 2: Provider + Config

// 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

// 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**
// v5 ONLY supports this:
useQuery({ queryKey, queryFn, ...options })
useMutation({ mutationFn, ...options })


βœ… **Use array query keys**
queryKey: ['todos']              // List
queryKey: ['todos', id] // Detail
queryKey: ['todos', { filter }] // Filtered


βœ… **Configure staleTime appropriately**
staleTime: 1000 * 60 * 5 // 5 min - prevents excessive refetches


βœ… **Use isPending for initial loading state**
if (isPending) return <Loading />
// isPending = no data yet AND fetching


βœ… **Throw errors in queryFn**
if (!response.ok) throw new Error('Failed')


βœ… **Invalidate queries after mutations**
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
}


βœ… **Use queryOptions factory for reusable patterns**
const opts = queryOptions({ queryKey, queryFn })
useQuery(opts)
useSuspenseQuery(opts)
prefetchQuery(opts)


βœ… **Use gcTime (not cacheTime)**
gcTime: 1000 * 60 * 60 // 1 hour


### Never Do

❌ **Never use v4 array/function syntax**
// v4 (removed in v5):
useQuery(['todos'], fetchTodos, options) // ❌

// v5 (correct):
useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) // βœ…


❌ **Never use query callbacks (onSuccess, onError, onSettled in queries)**
// v5 removed these from queries:
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
onSuccess: (data) => {}, // ❌ Removed in v5
})

// Use useEffect instead:
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
if (data) {
// Do something
}
}, [data])

// Or use mutation callbacks (still supported):
useMutation({
mutationFn: addTodo,
onSuccess: () => {}, // βœ… Still works for mutations
})


❌ **Never use deprecated options**
// Deprecated in v5:
cacheTime: 1000 // ❌ Use gcTime instead
isLoading: true // ❌ Meaning changed, use isPending
keepPreviousData: true // ❌ Use placeholderData instead
onSuccess: () => {} // ❌ Removed from queries
useErrorBoundary: true // ❌ Use throwOnError instead


❌ **Never assume isLoading means "no data yet"**
// v5 changed this:
isLoading = isPending && isFetching // ❌ Now means "pending AND fetching"
isPending = no data yet // βœ… Use this for initial load


❌ **Never forget initialPageParam for infinite queries**
// v5 requires this:
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam }) => fetchProjects(pageParam),
initialPageParam: 0, // βœ… Required in v5
getNextPageParam: (lastPage) => lastPage.nextCursor,
})


❌ **Never use enabled with useSuspenseQuery**
// Not allowed:
useSuspenseQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
enabled: !!id, // ❌ Not available with suspense
})

// Use conditional rendering instead:
{id && <TodoComponent id={id} />}


❌ **Never rely on refetchOnMount: false for errored queries**
// Doesn't work - errors are always stale
useQuery({
queryKey: ['data'],
queryFn: failingFetch,
refetchOnMount: false, // ❌ Ignored when query has error
})

// Use retryOnMount instead
useQuery({
queryKey: ['data'],
queryFn: failingFetch,
refetchOnMount: false,
retryOnMount: false, // βœ… Prevents refetch for errored queries
retry: 0,
})


---

## 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](https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#removed-overloads-in-favor-of-object-syntax)
**Why It Happens**: v5 removed all function overloads, only object syntax works
**Prevention**: Always use useQuery({ queryKey, queryFn, ...options })

**Before (v4):**
useQuery(['todos'], fetchTodos, { staleTime: 5000 })


**After (v5):**
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5000
})


### Issue #2: Query Callbacks Removed
**Error**: Callbacks don't run, TypeScript errors
**Source**: [v5 Breaking Changes](https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#callbacks-on-usequery-and-queryobserver-have-been-removed)
**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):**
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
onSuccess: (data) => {
console.log('Todos loaded:', data)
},
})


**After (v5):**
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
if (data) {
console.log('Todos loaded:', data)
}
}, [data])


### Issue #3: Status Loading β†’ Pending
**Error**: UI shows wrong loading state
**Source**: [v5 Migration: isLoading renamed](https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#isloading-and-isfetching-flags)
**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):**
const { data, isLoading } = useQuery(...)
if (isLoading) return <div>Loading...</div>


**After (v5):**
const { data, isPending, isLoading } = useQuery(...)
if (isPending) return <div>Loading...</div>
// isLoading = isPending && isFetching (fetching for first time)


### Issue #4: cacheTime β†’ gcTime
**Error**: cacheTime is not a valid option
**Source**: [v5 Migration: gcTime](https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#cachetime-has-been-replaced-by-gctime)
**Why It Happens**: Renamed to better reflect "garbage collection time"
**Prevention**: Use gcTime instead of cacheTime

**Before (v4):**
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
cacheTime: 1000 * 60 * 60,
})


**After (v5):**
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
gcTime: 1000 * 60 * 60,
})


### Issue #5: useSuspenseQuery + enabled
**Error**: Type error, enabled option not available
**Source**: [GitHub Discussion #6206](https://github.com/TanStack/query/discussions/6206)
**Why It Happens**: Suspense guarantees data is available, can't conditionally disable
**Prevention**: Use conditional rendering instead of enabled option

**Before (v4/incorrect):**
useSuspenseQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
enabled: !!id, // ❌ Not allowed
})


**After (v5/correct):**
// Conditional rendering:
{id ? (
<TodoComponent id={id} />
) : (
<div>No ID selected</div>
)}

// Inside TodoComponent:
function TodoComponent({ id }: { id: number }) {
const { data } = useSuspenseQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
// No enabled option needed
})
return <div>{data.title}</div>
}


### Issue #6: initialPageParam Required
**Error**: initialPageParam is required type error
**Source**: [v5 Migration: Infinite Queries](https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#new-required-initialPageParam-option)
**Why It Happens**: v4 passed undefined as first pageParam, v5 requires explicit value
**Prevention**: Always specify initialPageParam for infinite queries

**Before (v4):**
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam = 0 }) => fetchProjects(pageParam),
getNextPageParam: (lastPage) => lastPage.nextCursor,
})


**After (v5):**
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam }) => fetchProjects(pageParam),
initialPageParam: 0, // βœ… Required
getNextPageParam: (lastPage) => lastPage.nextCursor,
})


### Issue #7: keepPreviousData Removed
**Error**: keepPreviousData is not a valid option
**Source**: [v5 Migration: placeholderData](https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#removed-keeppreviousdata-in-favor-of-placeholderdata-identity-function)
**Why It Happens**: Replaced with more flexible placeholderData function
**Prevention**: Use placeholderData: keepPreviousData helper

**Before (v4):**
useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
keepPreviousData: true,
})


**After (v5):**
import { keepPreviousData } from '@tanstack/react-query'

useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
placeholderData: keepPreviousData,
})


### Issue #8: TypeScript Error Type Default
**Error**: Type errors with error handling
**Source**: [v5 Migration: Error Types](https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5#typeerror-is-now-the-default-error)
**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):**
const { error } = useQuery({
queryKey: ['data'],
queryFn: async () => {
if (Math.random() > 0.5) throw 'custom error string'
return data
},
})
// error: unknown


**After (v5 - specify custom error type):**
const { error } = useQuery<DataType, string>({
queryKey: ['data'],
queryFn: async () => {
if (Math.random() > 0.5) throw 'custom error string'
return data
},
})
// error: string | null

// Or better: always throw Error objects
const { error } = useQuery({
queryKey: ['data'],
queryFn: async () => {
if (Math.random() > 0.5) throw new Error('custom error')
return data
},
})
// 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](https://github.com/TanStack/query/issues/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):**
// Server: void prefetch
streamingQueryClient.prefetchQuery({ queryKey: ['data'], queryFn: getData });

// Client: conditional render on fetchStatus
const { data, isFetching } = useSuspenseQuery({ queryKey: ['data'], queryFn: getData });
return <>{data && <div>{data}</div>} {isFetching && <Loading />}</>;


**After (workaround):**
// Option 1: Await prefetch
await streamingQueryClient.prefetchQuery({ queryKey: ['data'], queryFn: getData });

// Option 2: Don't render based on fetchStatus with Suspense
const { data } = useSuspenseQuery({ queryKey: ['data'], queryFn: getData });
return <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](https://github.com/TanStack/query/issues/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):**
// Server Component
const queryClient = getServerQueryClient();
await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos });

// Client Component
function Todos() {
const { data, isLoading } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
if (isLoading) return <div>Loading...</div>; // Server renders this
return <div>{data.length} todos</div>; // Client hydrates with this
}


**After (workaround):**
// Use useSuspenseQuery instead
function Todos() {
const { data } = useSuspenseQuery({ queryKey: ['todos'], queryFn: fetchTodos });
return <div>{data.length} todos</div>;
}


**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](https://github.com/TanStack/query/issues/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):**
const { data, error } = useQuery({
queryKey: ['data'],
queryFn: () => { throw new Error('Fails') },
refetchOnMount: false, // Ignored when query is in error state
retry: 0,
});
// Query refetches every time component mounts


**After (correct):**
const { data, error } = useQuery({
queryKey: ['data'],
queryFn: failingFetch,
refetchOnMount: false,
retryOnMount: false, // βœ… Prevents refetch on mount for errored queries
retry: 0,
});


**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](https://github.com/TanStack/query/issues/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):**
useMutation({
mutationFn: addTodo,
onError: (error, variables, context) => {
// context is now onMutateResult, missing final context param
},
onSuccess: (data, variables, context) => {
// Same issue
}
});


**After (v5.89.0+):**
useMutation({
mutationFn: addTodo,
onError: (error, variables, onMutateResult, context) => {
// onMutateResult = return value from onMutate
// context = mutation function context
},
onSuccess: (data, variables, onMutateResult, context) => {
// Correct signature with 4 parameters
}
});


**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](https://github.com/TanStack/query/issues/9871) | Fixed in [PR #9872](https://github.com/TanStack/query/pull/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):**
export function todoQueryKey(id?: string) {
return id ? ['todos', id] as const : ['todos'] as const;
}
// Type: readonly ['todos', string] | readonly ['todos']

useMutation({
mutationFn: addTodo,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: todoQueryKey('123')
// Error: readonly ['todos', string] not assignable to ['todos', string]
});
}
});


**After (v5.90.9+):**
// Works correctly with readonly types
queryClient.invalidateQueries({
queryKey: todoQueryKey('123') // βœ… No type error
});


**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](https://github.com/TanStack/query/issues/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):**
const addTodo = useMutation({
mutationKey: ['addTodo'],
mutationFn: (todo: Todo) => api.addTodo(todo),
});

const pendingTodos = useMutationState({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => {
return mutation.state.variables; // Type: unknown
},
});


**After (with explicit cast):**
const pendingTodos = useMutationState({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => mutation.state.variables as Todo,
});
// Or cast the entire state:
select: (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](https://github.com/TanStack/query/issues/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:**
async function loadData() {
try {
const data = await queryClient.fetchQuery({
queryKey: ['data'],
queryFn: fetchData,
});
console.log('Loaded:', data); // Never logs in StrictMode
} catch (error) {
console.error('Failed:', error); // CancelledError
}
}

function Component() {
const { data } = useQuery({ queryKey: ['data'], queryFn: fetchData });
// In StrictMode, component unmounts/remounts, cancelling fetchQuery
}


**Workaround:**
// Keep query observed with staleTime
const { data } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
staleTime: Infinity, // Keeps query active
});


**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](https://github.com/TanStack/query/issues/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:**
// Only active queries (currently being observed) will refetch
queryClient.invalidateQueries({ queryKey: ['todos'] });


**To refetch inactive queries:**
queryClient.invalidateQueries({
queryKey: ['todos'],
refetchType: 'all' // Refetch active AND inactive
});


**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](https://tkdodo.eu/blog/react-query-api-design-lessons-learned) | **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:**
// Component A mounts first
function ComponentA() {
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5000, // Applied initially
});
}

// Component B mounts while A's query is in-flight
function ComponentB() {
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 60000, // Won't affect current fetch, only future ones
});
}


**Recommended approach:**
// Write options as functions that reference latest values
const getStaleTime = () => shouldUseLongCache ? 60000 : 5000;

useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: getStaleTime(), // Evaluated on each render
});


### Tip: refetch() is NOT for Changed Parameters

**Source**: [Avoiding Common Mistakes with TanStack Query](https://www.buncolak.com/posts/avoiding-common-mistakes-with-tanstack-query-part-1/) | **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:**
// ❌ Wrong - using refetch() for different parameters
const [page, setPage] = useState(1);
const { data, refetch } = useQuery({
queryKey: ['todos'], // Same key for all pages
queryFn: () => fetchTodos(page),
});

// This refetches with OLD page value, not new one
<button onClick={() => { setPage(2); refetch(); }}>Next</button>


**Correct pattern:**
// βœ… Correct - include parameters in query key
const [page, setPage] = useState(1);
const { data } = useQuery({
queryKey: ['todos', page], // Key changes with page
queryFn: () => fetchTodos(page),
// Query automatically refetches when page changes
});

<button onClick={() => setPage(2)}>Next</button> // Just update state


**When to use refetch():**
// βœ… Manual refresh of same data (refresh button)
const { data, refetch } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
});

<button onClick={() => refetch()}>Refresh</button> // Same parameters


---

## Key Patterns

**Dependent Queries** (Query B waits for Query A):
const { data: posts } = useQuery({
queryKey: ['users', userId, 'posts'],
queryFn: () => fetchUserPosts(userId),
enabled: !!user, // Wait for user
})


**Parallel Queries** (fetch multiple at once):
const results = useQueries({
queries: ids.map(id => ({ queryKey: ['todos', id], queryFn: () => fetchTodo(id) })),
})


**Prefetching** (preload on hover):
queryClient.prefetchQuery({ queryKey: ['todo', id], queryFn: () => fetchTodo(id) })


**Infinite Scroll** (useInfiniteQuery):
useInfiniteQuery({
queryKey: ['todos', 'infinite'],
queryFn: ({ pageParam }) => fetchTodosPage(pageParam),
initialPageParam: 0, // Required in v5
getNextPageParam: (lastPage) => lastPage.nextCursor,
})


**Query Cancellation** (auto-cancel on queryKey change):
queryFn: async ({ signal }) => {
const res = await fetch(/api/todos?q=${search}, { signal })
return res.json()
}


**Data Transformation** (select):
select: (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


---

---
paths: "**/*.tsx", "**/*.ts", "**/*query*.ts", "**/*hook*.ts"
---

# TanStack Query v5 Corrections

Claude's training may reference TanStack Query v4 patterns. This project uses **v5**.

## Object Syntax Required

/* ❌ v4 array syntax (removed in v5) */
useQuery(['todos'], fetchTodos, { staleTime: 5000 })

/* βœ… v5 object syntax only */
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5000
})


## Query Callbacks Removed

/* ❌ v5 removed onSuccess/onError/onSettled from queries */
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
onSuccess: (data) => console.log(data), // Removed!
})

/* βœ… Use useEffect instead */
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
if (data) console.log(data)
}, [data])

/* Note: Mutations still support callbacks */
useMutation({
mutationFn: addTodo,
onSuccess: () => {}, // Still works!
})


## isPending vs isLoading

/* ❌ isLoading meaning changed in v5 */
if (isLoading) return <Loading />

/* βœ… Use isPending for initial load */
const { data, isPending } = useQuery(...)
if (isPending) return <Loading />
// isPending = no data yet
// isLoading = isPending && isFetching


## cacheTime β†’ gcTime

/* ❌ Renamed in v5 */
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
cacheTime: 1000 * 60 * 60, // Error!
})

/* βœ… Use gcTime */
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
gcTime: 1000 * 60 * 60,
})


## keepPreviousData β†’ placeholderData

/* ❌ Removed in v5 */
useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
keepPreviousData: true, // Error!
})

/* βœ… Use placeholderData helper */
import { keepPreviousData } from '@tanstack/react-query'

useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
placeholderData: keepPreviousData,
})


## Infinite Queries: initialPageParam Required

/* ❌ v4 used undefined as first pageParam */
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam = 0 }) => fetchProjects(pageParam),
getNextPageParam: (lastPage) => lastPage.nextCursor,
})

/* βœ… v5 requires explicit initialPageParam */
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam }) => fetchProjects(pageParam),
initialPageParam: 0, // Required!
getNextPageParam: (lastPage) => lastPage.nextCursor,
})


## Quick Fixes

| If Claude suggests... | Use instead... |
|----------------------|----------------|
| useQuery(['key'], fn, opts) | useQuery({ queryKey, queryFn, ...opts }) |
| onSuccess in useQuery | useEffect watching data |
| cacheTime | gcTime |
| isLoading for initial load | isPending |
| keepPreviousData: true | placeholderData: keepPreviousData |
| Missing initialPageParam | Add initialPageParam: 0 (or appropriate value) |
| useErrorBoundary | throwOnError |

How to Use This Skill Unit

Option A: Project-Specific (Recommended)

  1. Click "Download" above
  2. In your project, create the directory: .agent/skills/tanstack-query/
  3. Save the file as SKILL.md
  4. The agent will automatically discover the skill based on its description.

Option B: Global Installation (All Agents)

Save the file to these locations to make it available across all projects:

  • Claude Code: ~/.claude/skills/jezweb/claude-skills/tanstack-query/SKILL.md
  • Cursor: ~/.cursor/skills/jezweb/claude-skills/tanstack-query/SKILL.md
  • Antigravity: ~/.gemini/antigravity/skills/jezweb/claude-skills/tanstack-query/SKILL.md

πŸš€ Install with CLI:
npx skills add jezweb/claude-skills

Read the Master Guide: Mastering Agent Skills β†’

Recommended Rules

View more rules β†’

Recommended Workflows

View more workflows β†’

Recommended MCP Servers

View more MCP servers β†’

Take It Further

Maximize your productivity with these powerful resources

πŸ“‹

Define Your Standards

Set up coding standards to ensure this workflow produces consistent, high-quality results.

Browse Rules Library
πŸ“–

Master Workflows

Learn how to create custom workflows, use Turbo Mode, and build your automation library.

Complete Guide

How to use this Skill in Claude Code & Cursor

For Claude Code (CLI)

To use this skill in Claude Code, copy the rule content into your project's custom instructions or follow our Add-Skill CLI guide. This ensures Claude follows your standards during every code generation.

For Cursor & Windsurf

For Cursor or Windsurf, individual skills are best used in the "Rules for AI" section. This specific unit helps the agent avoid react & react native issues, leading to cleaner, more efficient code.

Why the skill format matters: the standardized Agent Skills format lets your AI agent load detailed instructions only when they are relevant, keeping your prompt clean while improving results.

Source & attribution

This skill is categorized under React & React Native and is published by JezWeb, maintained in jezweb/claude-skills.

← Browse All Agent Skills
Sponsored AI assistant. Recommendations may be paid.