Configure staleTime to avoid excessive refetches (default is 0)
Use gcTime (not - renamed in v5)
cacheTime
DevTools should be inside provider
Know the defaults (v5):
staleTime: 0 → data is immediately stale, so refetches on mount/focus unless you raise it
gcTime: 5 * 60 * 1000 → inactive data is garbage-collected after 5 minutes
retry: 3 in browsers, retry: 0 on the server
refetchOnWindowFocus: true and refetchOnReconnect: true
networkMode: 'online' (requests pause while offline). Switch to 'always' for SSR/prefetch where you don't want cancellation. citeturn1search0turn1search1
3. Create First Query
// src/hooks/useTodos.ts
import { useQuery } from '@tanstack/react-query'
type Todo = {
id: number
title: string
completed: boolean
}
async function fetchTodos(): Promise<Todo[]> {
const response = await fetch('/api/todos')
if (!response.ok) {
throw new Error('Failed to fetch todos')
}
return response.json()
}
export function useTodos() {
return useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
}
// Usage in component:
function TodoList() {
const { data, isPending, isError, error } = useTodos()
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>
)
}
@tanstack/eslint-plugin-query - Catches common mistakes
Version requirements:
React 18.0 or higher (uses useSyncExternalStore)
TypeScript 5.2+ for best type inference (optional but recommended)
Step 2: Configure QueryClient
// src/lib/query-client.ts
import { QueryClient } from '@tanstack/react-query'
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
// How long data is considered fresh (won't refetch during this time)
staleTime: 1000 * 60 * 5, // 5 minutes
// How long inactive data stays in cache before garbage collection
gcTime: 1000 * 60 * 60, // 1 hour (v5: renamed from cacheTime)
// Retry failed requests (0 on server, 3 on client by default)
retry: (failureCount, error) => {
if (error instanceof Response && error.status === 404) return false
return failureCount < 3
},
// Refetch on window focus (can be annoying during dev)
refetchOnWindowFocus: false,
// Refetch on network reconnect
refetchOnReconnect: true,
// Refetch on component mount if data is stale
refetchOnMount: true,
},
mutations: {
// Retry mutations on failure (usually don't want this)
retry: 0,
},
},
})
Key configuration decisions:
staleTime vs gcTime:
staleTime: How long until data is considered "stale" and might refetch
0 (default): Data is immediately stale, refetches on mount/focus
1000 * 60 * 5: Data fresh for 5 min, no refetch during this time
Infinity: Data never stale, manual invalidation only
gcTime: How long unused data stays in cache
1000 * 60 * 5 (default): 5 minutes
Infinity: Never garbage collect (memory leak risk)
When to refetch:
refetchOnWindowFocus: true - Good for frequently changing data (stock prices)
refetchOnWindowFocus: false - Good for stable data or during development
refetchOnMount: true - Ensures fresh data when component mounts
refetchOnReconnect: true - Refetch after network reconnect
Step 3: Wrap App with Provider
// src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { queryClient } from './lib/query-client'
import App from './App'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools
initialIsOpen={false}
buttonPosition="bottom-right"
/>
</QueryClientProvider>
</StrictMode>
)
Provider placement:
Must wrap all components that use TanStack Query hooks
DevTools must be inside provider
Only one QueryClient instance for entire app
DevTools configuration:
initialIsOpen={false} - Collapsed by default
buttonPosition="bottom-right" - Where to show toggle button
Automatically removed in production builds (tree-shaken)
Advanced Setup (Steps 4-7)
For detailed patterns: Load references/advanced-setup.md when implementing custom query hooks, mutations with optimistic updates, DevTools configuration, or error boundaries.
Quick summaries:
Step 4: Custom Query Hooks - Use queryOptions factory for reusable patterns. Create custom hooks that encapsulate API calls.
Step 5: Mutations - Use useMutation with onSuccess to invalidate queries. For instant UI feedback, implement optimistic updates with onMutate/onError/onSettled pattern.
Step 6: DevTools - Already included in Step 3. Advanced options for customization available in reference.
Step 7: Error Boundaries - Use QueryErrorResetBoundary with React Error Boundary. Configure throwOnError option for global vs local error handling.
react-native.tsx - offline-first with AsyncStorage persister
When Claude should load these:
advanced-setup.md - When implementing custom query hooks, mutations, or error boundaries
configuration-files.md - When setting up new projects or troubleshooting build/type errors
v4-to-v5-migration.md - When migrating existing React Query v4 project
best-practices.md - When optimizing performance or avoiding waterfalls
common-patterns.md - When implementing specific features (infinite scroll, etc.)
typescript-patterns.md - When dealing with TypeScript errors or type inference
testing.md - When writing tests for components using TanStack Query
top-errors.md - When encountering errors not covered in main SKILL.md
Advanced Topics
Data Transformations with select
// Only subscribe to specific slice of data
function TodoCount() {
const { data: count } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (data) => data.length, // Only re-render when count changes
})
return <div>Total todos: {count}</div>
}
// Transform data shape
function CompletedTodoTitles() {
const { data: titles } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (data) =>
data
.filter(todo => todo.completed)
.map(todo => todo.title),
})
return (
<ul>
{titles?.map((title, i) => (
<li key={i}>{title}</li>
))}
</ul>
)
}
Benefits:
Component only re-renders when selected data changes
Reduces memory usage (less data stored in component state)
Keeps query cache unchanged (other components get full data)
Request Waterfalls (Anti-Pattern)
// ❌ BAD: Sequential waterfalls
function BadUserProfile({ userId }: { userId: number }) {
const { data: user } = useQuery({
queryKey: ['users', userId],
queryFn: () => fetchUser(userId),
})
const { data: posts } = useQuery({
queryKey: ['posts', user?.id],
queryFn: () => fetchPosts(user!.id),
enabled: !!user,
})
const { data: comments } = useQuery({
queryKey: ['comments', posts?.[0]?.id],
queryFn: () => fetchComments(posts![0].id),
enabled: !!posts && posts.length > 0,
})
// Each query waits for previous one = slow!
}
// ✅ GOOD: Fetch in parallel when possible
function GoodUserProfile({ userId }: { userId: number }) {
const { data: user } = useQuery({
queryKey: ['users', userId],
queryFn: () => fetchUser(userId),
})
// Fetch posts AND comments in parallel
const { data: posts } = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchPosts(userId), // Don't wait for user
})
const { data: comments } = useQuery({
queryKey: ['comments', userId],
queryFn: () => fetchUserComments(userId), // Don't wait for posts
})
// All 3 queries run in parallel = fast!
}
Server State vs Client State
// ❌ Don't use TanStack Query for client-only state
const { data: isModalOpen, setData: setIsModalOpen } = useMutation(...)
// ✅ Use useState for client state
const [isModalOpen, setIsModalOpen] = useState(false)
// ✅ Use TanStack Query for server state
const { data: todos } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
Rule of thumb:
Server state: Use TanStack Query (data from API)
Client state: Use useState/useReducer (local UI state)
Global client state: Use Zustand/Context (theme, auth token)
Platform & Integration Notes
React Native: Works the same as web. Use @tanstack/query-async-storage-persister to persist cache to AsyncStorage; avoid window-focus refetch logic. DevTools panel not available natively—use Flipper or expose logs.
GraphQL: Pair with graphql-request or urql's bare client. Treat operations as plain async functions; co-locate fragments and use select to map edges/nodes to flat shapes.
SSR / Next.js / TanStack Start: Use dehydrate/HydrationBoundary on the server and QueryClientProvider on the client. Set networkMode: 'always' for server prefetches so requests are never paused.
Suspense: Prefer useSuspenseQuery for routes already using Suspense. Do not combine with enabled; gate rendering instead.
Testing: Use @testing-library/react + @tanstack/react-query/testing helpers and mock network with MSW. Reset QueryClient between tests to avoid cache bleed.