MCPInstall

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.

Installs
2.5K
Last 24h
—
Trending rank
—
Security
Not audited

Install

Works with every agent the skills CLI supports.

npx skills add https://github.com/jezweb/claude-skills --skill tanstack-query
  1. 01

    Install

    Run the command in your project. The CLI asks which agents to install for.

  2. 02

    Describe the task

    Ask your agent for something this skill covers. It loads tanstack-query automatically when relevant.

  3. 03

    Review the result

    The agent follows the SKILL.md steps, scripts and references bundled with the skill.

SKILL.md

The instructions your agent reads when the skill is triggered.

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:

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)
})
ModeBehavior
online (default)Only fetch when online
alwaysAlways try (useful for local/service worker APIs)
offlineFirstUse 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 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 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 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 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 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 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 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 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 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 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 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 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 | 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):

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

FAQ

What does the tanstack-query skill do?

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.

How do I install tanstack-query?

Run npx skills add https://github.com/jezweb/claude-skills --skill tanstack-query in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/tanstack-query/ (personal) or .claude/skills/tanstack-query/ (project).

Which AI agents can use tanstack-query?

Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.

Is tanstack-query safe to install?

No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.

Who maintains tanstack-query?

It is published by jezweb in jezweb/claude-skills. It has 2,513 recorded installs.

More from jezweb/claude-skills

View all →

shadcn-ui

jezweb/claude-skills

Install and configure shadcn/ui components for React projects. Guides component selection, installation order, dependency management, customisation with semantic tokens, and common UI recipes (forms, data tables, navigation, modals). Use after tailwind-theme-builder has set up the theme infrastructure, when adding components, building forms, creating data tables, or setting up navigation.

Frontend & Design3.6K installs

color-palette

jezweb/claude-skills

Generate complete, accessible colour palettes from a single brand hex. Produces 11-shade scale (50-950), semantic tokens, dark mode variants, Tailwind v4 CSS output, WCAG contrast checks. Use whenever the user supplies a brand hex and asks for a palette, mentions setting up a design system, wants Tailwind theme colours from a brand colour, or asks to check colour accessibility / contrast.

Frontend & Design3.2K installs

tailwind-theme-builder

jezweb/claude-skills

Set up Tailwind v4 + shadcn/ui themed UI with dark mode. Install deps, configure CSS variables via @theme inline, wire dark mode toggle, verify. Use whenever the user mentions Tailwind v4, setting up Tailwind theming, shadcn/ui colours, dark mode, or troubleshooting colours not working, tw-animate-css errors, @theme inline conflicts, @apply breaking after upgrade, or v3 → v4 migration issues.

Frontend & Design3.2K installs

wordpress-elementor

jezweb/claude-skills

Edit Elementor pages and manage templates on WordPress sites via browser automation (for visual / structural changes) or WP-CLI (for safe text replacements). Use whenever the user wants to edit an Elementor page, update text in Elementor widgets, apply or manage Elementor templates, or make content changes to pages built with the Elementor page builder.

ux-audit

jezweb/claude-skills

Walk through a live web app AS a real user to find usability + behavioural bugs that static reviews miss. REQUIRES proof of interaction (typing, clicking, sending, observing) before any verdict — a sweep that didn't interact terminates with verdict 'Incomplete'. Walks threads, exercises every element, runs the multi-pane stress matrix, visual polish sweep, component perfection checklist, automated a11y (axe-core), pragmatic performance budget (LCP/CLS/INP), scenario battery (11 scenarios), and stress recipes including the real-flavour data battery. Hard gates: console errors/warnings = 0, network 5xx = 0, layout collapse = 0, axe Critical/Serious = 0, perf budget green. Audit-the-audit meta-check rejects rushed reports. Each finding has reproduction steps, evidence path, and suspected code location. Trigger with 'ux audit', 'walkthrough', 'qa sweep', 'audit the app', 'dogfood this', 'check all pages', 'find what's broken', 'stress the UI'.

Frontend & Design2.8K installs

tailwind-v4-shadcn

jezweb/claude-skills

Set up Tailwind v4 with shadcn/ui using @theme inline pattern and CSS variable architecture. Four-step pattern: CSS variables, Tailwind mapping, base styles, automatic dark mode. Prevents 8 documented errors. Use when initializing React projects with Tailwind v4, or fixing colors not working, tw-animate-css errors, @theme inline dark mode conflicts, @apply breaking, v3 migration issues.

Frontend & Design2.6K installs

Popular backend & apis skills

View category →

find-skills

vercel-labs/skills

Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.

OfficialBackend & APIs4.1M installs

entra-app-registration

microsoft/azure-skills

Guides Microsoft Entra ID app registration, OAuth 2.0 authentication, and MSAL integration. USE FOR: create app registration, register Azure AD app, configure OAuth, set up authentication, add API permissions, generate service principal, MSAL example, console app auth, Entra ID setup, Azure AD authentication. DO NOT USE FOR: Key Vault secrets (use azure-keyvault-expiration-audit), general Azure resource security guidance.

OfficialBackend & APIs566.7K installs

This skill should be used when the user wants to "write agent code", "build an agent with ADK", "add a tool", "create a callback", "define an agent", "use state management" — in a project that needs ADK (Agent Development Kit) API patterns and code examples. It provides a quick reference for agent types, tool definitions, orchestration patterns, callbacks, state management, the graph Workflow API, and reference recipes to study. Do NOT use for scaffolding (use google-agents-cli-scaffold) or deployment (use google-agents-cli-deploy).

Backend & APIs514.8K installs

embedded-captions

heygen-com/hyperframes

Add captions or subtitles to an existing single-subject talking-head video without editing the footage. Use for plain verbatim captions, cinematic captions embedded behind the subject, VFX captions, “炸/特效/酷炫字幕,” or a named identity from the 35-style catalog. Route by visual identity, not by backend engine. The quiet anchor rail is the default; embed every word only when the user explicitly wants a fully cinematic treatment. The workflow runs locally end to end, including transcription and subject matting; split multi-shot footage before applying it.

Backend & APIs453.3K installs

entra-agent-id

microsoft/azure-skills

Provision Microsoft Entra Agent Identity Blueprints, BlueprintPrincipals, and per-instance Agent Identities via Microsoft Graph, and configure OAuth 2.0 token exchange (fmipath, OBO, cross-tenant) including the Microsoft Entra SDK for AgentID sidecar. USE FOR: Agent Identity Blueprint, BlueprintPrincipal, agent OAuth, fmipath token exchange, agent OBO, Workload Identity Federation for agents, polyglot agent auth, Microsoft.Identity.Web.AgentIdentities. DO NOT USE FOR: standard Entra app registration (use entra-app-registration), Microsoft Foundry agent authoring (use microsoft-foundry).

OfficialBackend & APIs336.6K installs

neon

neondatabase/agent-skills

Overview of Neon, a complete set of cloud backend primitives around Lakebase Postgres: Auth, Object Storage, Functions, the AI Gateway, and Realtime. Start here to choose Neon for undecided login, files, APIs, and LLM calls, set up the CLI or MCP server, and follow the branch-first workflow. Use when building an app or backend on Neon, or when "Neon" or "Lakebase Postgres" is mentioned. Child skill neon-postgres wins for an existing DATABASEURL, SQL, schema, inspect, or search. Child skill neon-auth wins for login, users, sessions, identity routing, and Managed Better Auth setup. Child skill neon-realtime wins for live queries in reactive apps and agents. Also use for object storage, S3, buckets, serverless functions, function triggers, cron, AI gateway, LLM calls, realtime, logs, Loki, Grafana, observability, postgres, database, backend, Claimable Neon, neon.new, or a no-signup database.

Backend & APIs237.1K installs