TanStack Query DevTools provides real-time visibility into query cache state — ReactQueryDevtools renders a floating panel showing every active query, its status, data, and stale time. useQueryClient() gives programmatic access to the cache for manual invalidation, prefetching, and inspection in tests. queryClient.getQueryData(queryKey) reads cached data without triggering a fetch. queryClient.setQueryData manually updates cache entries. The logger option on QueryClient overrides console methods for structured logging. persistQueryClient with createSyncStoragePersister or createAsyncStoragePersister persists the cache to localStorage across page reloads. dehydrate(queryClient) snapshots the cache for SSR hydration debugging. queryClient.getQueryCache().findAll() lists all tracked queries. Claude Code generates React Query DevTools setup, cache debugging utilities, persisted query configuration, and structured error logging patterns.
CLAUDE.md for React Query Debugging
## TanStack Query Debug Stack
- Version: @tanstack/react-query >= 5.40, @tanstack/react-query-devtools >= 5.40
- DevTools: <ReactQueryDevtools initialIsOpen={false} buttonPosition="bottom-right" />
- Cache read: queryClient.getQueryData(["orders", { status }]) — no fetch
- Cache write: queryClient.setQueryData(key, updater) — instant cache update
- Invalidate: queryClient.invalidateQueries({ queryKey: ["orders"] }) — triggers refetch
- List all: queryClient.getQueryCache().findAll() — inspect active queries
- Persist: persistQueryClient({ queryClient, persister: createSyncStoragePersister({...}) })
- Logger: new QueryClient({ logger: { log, warn, error: customFn } })
DevTools Setup
// app/providers.tsx — QueryClient + DevTools
"use client"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
import { useState } from "react"
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 60_000, // 1 minute
gcTime: 5 * 60_000, // 5 minutes
retry: (failureCount, error) => {
// Don't retry 4xx errors
if (error instanceof Error && "status" in error) {
const status = (error as any).status as number
if (status >= 400 && status < 500) return false
}
return failureCount < 3
},
refetchOnWindowFocus: process.env.NODE_ENV === "production",
},
mutations: {
// Log all mutation errors in dev
onError: process.env.NODE_ENV === "development"
? (error, variables) => {
console.error("[Mutation Error]", { error, variables })
}
: undefined,
},
},
})
}
// Singleton on server, fresh per render on client (Next.js App Router pattern)
let browserQueryClient: QueryClient | undefined
function getQueryClient() {
if (typeof window === "undefined") return makeQueryClient()
if (!browserQueryClient) browserQueryClient = makeQueryClient()
return browserQueryClient
}
export function QueryProviders({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => getQueryClient())
return (
<QueryClientProvider client={queryClient}>
{children}
{process.env.NODE_ENV !== "production" && (
<ReactQueryDevtools
initialIsOpen={false}
buttonPosition="bottom-right"
// Show panel position
position="bottom"
/>
)}
</QueryClientProvider>
)
}
Cache Inspection Utilities
// lib/query-debug.ts — cache inspection helpers
import { useQueryClient, type QueryKey } from "@tanstack/react-query"
import { useCallback, useEffect } from "react"
// Hook to log all active queries in development
export function useQueryDebugger() {
const queryClient = useQueryClient()
useEffect(() => {
if (process.env.NODE_ENV !== "development") return
const cache = queryClient.getQueryCache()
const unsubscribe = cache.subscribe(event => {
if (event.type === "updated") {
const query = event.query
console.log(`[Query] ${JSON.stringify(query.queryKey)} → ${query.state.status}`, {
dataUpdatedAt: query.state.dataUpdatedAt
? new Date(query.state.dataUpdatedAt).toLocaleTimeString()
: null,
isStale: query.isStale(),
fetchCount: query.state.fetchFailureCount,
})
}
})
return unsubscribe
}, [queryClient])
}
// Get snapshot of all cached data — useful in browser console
export function useQuerySnapshot() {
const queryClient = useQueryClient()
return useCallback(() => {
const queries = queryClient.getQueryCache().findAll()
return queries.map(q => ({
key: q.queryKey,
status: q.state.status,
dataSize: JSON.stringify(q.state.data ?? null).length,
stale: q.isStale(),
updatedAt: q.state.dataUpdatedAt
? new Date(q.state.dataUpdatedAt).toISOString()
: null,
}))
}, [queryClient])
}
// Force-expire specific queries for testing
export function useQueryExpiry() {
const queryClient = useQueryClient()
return useCallback((queryKey: QueryKey) => {
queryClient.setQueryDefaults(queryKey, { staleTime: 0 })
queryClient.invalidateQueries({ queryKey })
}, [queryClient])
}
Persisted Cache
// lib/query-persister.ts — persist cache across page reloads
import { persistQueryClient, removeOldestQuery } from "@tanstack/react-query-persist-client"
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"
import type { QueryClient } from "@tanstack/react-query"
export function setupQueryPersistence(queryClient: QueryClient) {
if (typeof window === "undefined") return
const persister = createSyncStoragePersister({
storage: window.localStorage,
key: "rq-cache",
// Remove oldest queries when storage limit hit
retry: removeOldestQuery,
// Throttle writes to avoid excessive storage writes
throttleTime: 1_000,
// Custom serializer for Date objects
serialize: data => JSON.stringify(data),
deserialize: data => JSON.parse(data),
})
persistQueryClient({
queryClient,
persister,
maxAge: 24 * 60 * 60 * 1_000, // Discard cache older than 24 hours
// Only persist specific query keys
dehydrateOptions: {
shouldDehydrateQuery: query => {
const key = query.queryKey[0]
// Only cache user data and public content — skip sensitive queries
return ["products", "categories", "user-profile"].includes(key as string)
},
},
})
}
Error Boundary with Query Errors
// components/QueryErrorBoundary.tsx — catch query errors
"use client"
import { useQueryClient } from "@tanstack/react-query"
import { Component, type ReactNode } from "react"
interface Props {
children: ReactNode
queryKey?: unknown[]
fallback?: ReactNode
}
interface State { hasError: boolean }
export class QueryErrorBoundary extends Component<Props, State> {
state: State = { hasError: false }
static getDerivedStateFromError(): State {
return { hasError: true }
}
render() {
if (this.state.hasError) {
return this.props.fallback ?? (
<div className="p-4 border border-red-200 rounded-lg">
<p className="text-red-600 text-sm font-medium">Failed to load data</p>
<button
onClick={() => this.setState({ hasError: false })}
className="mt-2 text-sm text-primary hover:underline"
>
Retry
</button>
</div>
)
}
return this.props.children
}
}
// Programmatic error reset tied to query invalidation
export function useQueryErrorReset(queryKey: unknown[]) {
const queryClient = useQueryClient()
return {
resetError: () => {
queryClient.resetQueries({ queryKey, exact: true })
},
refetch: () => {
queryClient.invalidateQueries({ queryKey, exact: true })
},
}
}
Query Status Monitoring
// components/dev/QueryMonitor.tsx — visible query status panel (dev only)
"use client"
import { useQueryClient } from "@tanstack/react-query"
import { useState, useEffect } from "react"
interface QuerySummary {
key: string
status: string
stale: boolean
count: number
}
export function QueryMonitor() {
const queryClient = useQueryClient()
const [queries, setQueries] = useState<QuerySummary[]>([])
useEffect(() => {
const update = () => {
const all = queryClient.getQueryCache().findAll()
const grouped = new Map<string, QuerySummary>()
for (const q of all) {
const key = JSON.stringify(q.queryKey)
const existing = grouped.get(key)
if (!existing || q.state.dataUpdatedAt > (existing as any).__updatedAt) {
grouped.set(key, {
key: key.slice(0, 40),
status: q.state.status,
stale: q.isStale(),
count: (existing?.count ?? 0) + 1,
})
}
}
setQueries([...grouped.values()].slice(0, 10))
}
update()
const unsub = queryClient.getQueryCache().subscribe(update)
return unsub
}, [queryClient])
if (process.env.NODE_ENV === "production") return null
return (
<div className="fixed bottom-16 left-4 z-40 bg-black/90 text-white text-xs rounded-lg p-3 max-w-xs font-mono">
<p className="font-bold mb-2 text-yellow-400">Query Cache ({queries.length})</p>
{queries.map(q => (
<div key={q.key} className="flex gap-2 items-center mb-1">
<span className={`h-1.5 w-1.5 rounded-full flex-shrink-0 ${
q.status === "success" ? "bg-green-400" :
q.status === "error" ? "bg-red-400" :
q.status === "pending" ? "bg-yellow-400" : "bg-gray-400"
}`} />
<span className="truncate opacity-70">{q.key}</span>
{q.stale && <span className="text-orange-400 flex-shrink-0">stale</span>}
</div>
))}
</div>
)
}
For the SWR DevTools alternative when using Vercel’s SWR library — swr-devtools provides a similar cache inspector panel for SWR’s key-based cache but with a narrower feature set than ReactQueryDevtools, see the SWR debugging guide. For the Apollo Client DevTools alternative when using GraphQL with Apollo Client — Apollo’s browser DevTools extension provides query inspector, cache explorer, and mutation log that integrates directly into browser DevTools rather than rendering in the page, see the GraphQL client debugging guide. The Claude Skills 360 bundle includes TanStack Query skill sets covering DevTools, cache inspection, and persistence. Start with the free tier to try query debugging generation.