mcpField
T
tanstack-query

tanstack-query

github.com/marcioaltoeDevelopment063 installsUpdated today
Repository

About tanstack-query

A reference skill for TanStack Query v5 users working with React and TypeScript. It explains query and mutation patterns, caching, migration, SSR, offline support, and known version-specific issues.

Key features

  1. 01Query and mutation patterns
  2. 02Cache configuration and invalidation
  3. 03Optimistic updates and error handling
  4. 04SSR, pagination, and offline guidance
  5. 05v4-to-v5 migration notes

Use cases

  1. 01Implementing data fetching in a React app
  2. 02Updating a project from v4 to v5
  3. 03Configuring query caching and refresh
  4. 04Troubleshooting hydration or mutation behavior

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/marcioaltoe/skills --skill tanstack-query

After installing, type/tanstack-queryin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

agents/
openai.yaml
rules/
cache-configuration.md
cache-invalidation.md
err-error-handling.md
inf-infinite-queries.md
mut-basics.md
mut-optimistic-updates.md
offline-support.md
parallel-queries.md
perf-optimization.md
pf-prefetching.md
qk-query-keys.md
ssr-hydration.md

Details

From the project README

TanStack Query v5

Version: @tanstack/react-[email protected] Requires: React 18.0+, TypeScript 4.7+

v5 New Features

  • useMutationState — cross-component mutation tracking without prop drilling
  • Simplified optimistic updates — via variables from pending mutations, no cache manipulation needed
  • throwOnError — renamed from useErrorBoundary
  • networkMode — offline/PWA support (online | always | offlineFirst)
  • useQueries with combine — merge parallel query results into single object
  • infiniteQueryOptions — type-safe factory for infinite queries (parallel to queryOptions)
  • maxPages — limit pages in cache for infinite queries (requires bi-directional pagination)
  • Mutation callback signature change (v5.89+) — onError/onSuccess/onSettled now receive 4 params (added onMutateResult)

Quick Setup

npm install @tanstack/react-query@latest @tanstack/react-query-devtools@latest
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
      refetchOnWindowFocus: false,
    },
  },
})

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  )
}
Unified Devtools (Recommended with Multiple TanStack Libraries)

If using Query + Router (or other TanStack libraries), use the unified TanStackDevtools shell instead of individual devtools components:

npm install -D @tanstack/react-devtools
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
import { TanStackDevtools } from '@tanstack/react-devtools'

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
      <TanStackDevtools
        config={{ position: 'bottom-right' }}
        plugins={[
          { name: 'TanStack Query', render: <ReactQueryDevtoolsPanel /> },
          // Add more plugins: Router, etc.
        ]}
      />
    </QueryClientProvider>
  )
}

Use *Panel variants (ReactQueryDevtoolsPanel, TanStackRouterDevtoolsPanel) when embedding inside TanStackDevtools.

import { useQuery, useMutation, useQueryClient, queryOptions } from '@tanstack/react-query'

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()
  },
})

function useTodos() {
  return useQuery(todosQueryOptions)
}

function useAddTodo() {
  const queryClient = useQueryClient()
  return useMutation({
    mutationFn: async (newTodo: { title: string }) => {
      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'] })
    },
  })
}

Rule Categories

PriorityCategoryRule FileImpact
CRITICALQuery Keysrules/qk-query-keys.mdPrevents cache bugs and data inconsistencies
CRITICALCachingrules/cache-configuration.mdOptimizes performance and data freshness
HIGHInvalidationrules/cache-invalidation.mdEnsures stale data is properly refreshed
HIGHMutationsrules/mut-basics.mdEnsures data integrity after writes
HIGHOptimistic Updatesrules/mut-optimistic-updates.mdResponsive UI during mutations
HIGHError Handlingrules/err-error-handling.mdPrevents poor user experiences
MEDIUMPrefetchingrules/pf-prefetching.mdImproves perceived performance
MEDIUMInfinite Queriesrules/inf-infinite-queries.mdPrevents pagination bugs
MEDIUMSSR/Hydrationrules/ssr-hydration.mdEnables proper server rendering
MEDIUMParallel Queriesrules/parallel-queries.mdDynamic parallel fetching
LOWPerformancerules/perf-optimization.mdReduces unnecessary re-renders
LOWOffline Supportrules/offline-support.mdEnables offline-first patterns

Critical Rules

Always Do
  • Object syntax for all hooks: useQuery({ queryKey, queryFn, ...options })
  • Array query keys: ['todos'], ['todos', id], ['todos', { filter }]
  • Throw errors in queryFn: if (!res.ok) throw new Error('Failed')
  • isPending for initial loading: if (isPending) return <Loading />
  • Invalidate after mutations: onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] })
  • queryOptions factory: reuse across useQuery, useSuspenseQuery, prefetchQuery
  • gcTime (not cacheTime): renamed in v5
Never Do
  • v4 array/function syntax: useQuery(['todos'], fetchTodos) — removed in v5
  • Query callbacks: onSuccess/onError/onSettled removed from queries (still work in mutations) — use useEffect instead
  • isLoading for "no data yet": meaning changed in v5 — use isPending
  • enabled with useSuspenseQuery: not available — use conditional rendering
  • keepPreviousData: removed — use placeholderData: keepPreviousData
  • refetch() for changed parameters: include params in queryKey instead, query auto-refetches

v4→v5 Migration Cheatsheet

v4v5Notes
useQuery(['key'], fn, opts)useQuery({ queryKey, queryFn, ...opts })Object syntax only
cacheTimegcTimeRenamed
isLoading (no data)isPendingisLoading = isPending && isFetching
keepPreviousData: trueplaceholderData: keepPreviousDataImport keepPreviousData helper
useErrorBoundarythrowOnErrorRenamed
onSuccess/onError/onSettled on queriesRemovedUse useEffect for side effects
pageParam = 0 defaultinitialPageParam: 0Required for infinite queries
status: 'loading'status: 'pending'Renamed
onError(err, vars, ctx)onError(err, vars, onMutateResult, ctx)v5.89+ added 4th param

Known Issues (v5.90.x)

  • Streaming SSR hydration mismatch — void prefetchQuery + useSuspenseQuery with conditional isFetching render causes hydration errors. Workaround: await prefetch or don't render based on fetchStatus
  • useQuery hydration error with prefetching — useQuery + server prefetch can mismatch isLoading between server/client. Use useSuspenseQuery instead
  • refetchOnMount ignored for errored queries — errors are always stale. Use retryOnMount: false in addition to refetchOnMount: false
  • useMutationState types — mutation.state.variables typed as unknown due to fuzzy matching. Cast explicitly in select callback
  • invalidateQueries only refetches active queries — use refetchType: 'all' to include inactive queries
  • Readonly query keys break in v5.90.8 — fixed in v5.90.9+

Key Patterns

// Dependent queries (B waits for A)
const { data: user } = useQuery({ queryKey: ['user', id], queryFn: () => fetchUser(id) })
const { data: posts } = useQuery({
  queryKey: ['posts', user?.id],
  queryFn: () => fetchPosts(user!.id),
  enabled: !!user,
})

// Parallel queries
const results = useQueries({
  queries: ids.map(id => ({ queryKey: ['item', id], queryFn: () => fetchItem(id) })),
  combine: (results) => ({ data: results.map(r => r.data), pending: results.some(r => r.isPending) }),
})

// Prefetch on hover
const handleHover = () => queryClient.prefetchQuery({ queryKey: ['item', id], queryFn: () => fetchItem(id) })

// Infinite scroll
useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: ({ pageParam }) => fetchPosts(pageParam),
  initialPageParam: 0,
  getNextPageParam: (lastPage) => lastPage.nextCursor,
})

// Query cancellation
queryFn: async ({ signal }) => {
  const res = await fetch(`/api/search?q=${query}`, { signal })
  return res.json()
}

// Data transformation
useQuery({ queryKey: ['todos'], queryFn: fetchTodos, select: (data) => data.filter(t => t.completed) })

Comments

Markdown supported