TanStack Query (React Query) v5
Status: Production Ready ✅ Last Updated: 2025-12-09 Dependencies: React 18.0+ (18.3+ recommended), TypeScript 4.9+ (5.x preferred) Latest Versions: @tanstack/react-query@5.90.12, @tanstack/react-query-devtools@5.91.1, @tanstack/eslint-plugin-query@5.91.2
Quick Start (5 Minutes)
1. Install Dependencies
# choose your package manager
pnpm add @tanstack/react-query@latest @tanstack/react-query-devtools@latest
# or
npm install @tanstack/react-query@latest @tanstack/react-query-devtools@latest
# or
bun add @tanstack/react-query@latest @tanstack/react-query-devtools@latest
Why this matters:
- TanStack Query v5 requires React 18+ (uses useSyncExternalStore)
- DevTools are essential for debugging queries and mutations
- v5 has breaking changes from v4 - use latest for all fixes
2. Set Up QueryClient Provider
// src/main.tsx or src/index.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import App from './App'
// Create a client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 60, // 1 hour (formerly cacheTime)
retry: 1,
refetchOnWindowFocus: false,
},
},
})
createRoot(document.getElementById('root')!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</StrictMode>
)
CRITICAL:
- Wrap entire app with
QueryClientProvider - Configure
staleTimeto avoid excessive refetches (default is 0) - Use
gcTime(notcacheTime- renamed in v5) - DevTools should be inside provider
Know the defaults (v5):
staleTime: 0→ data is immediately stale, so refetches on mount/focus unless you raise itgcTime: 5 60 1000→ inactive data is garbage-collected after 5 minutesretry: 3in browsers,retry: 0on the serverrefetchOnWindowFocus: trueandrefetchOnReconnect: truenetworkMode: '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>
)
}
CRITICAL:
- v5 requires object syntax:
useQuery({ queryKey, queryFn }) - Use
isPending(notisLoading- that now means "pending AND fetching") - Always throw errors in queryFn for proper error handling
- QueryKey should be array for consistent cache keys
4. Create First Mutation
// src/hooks/useAddTodo.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'
type NewTodo = {
title: string
}
async function addTodo(newTodo: NewTodo) {
const response = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodo),
})
if (!response.ok) throw new Error('Failed to add todo')
return response.json()
}
export function useAddTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: addTodo,
onSuccess: () => {
// Invalidate and refetch todos
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
}
// Usage in component:
function AddTodoForm() {
const { mutate, isPending } = useAddTodo()
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
mutate({ title: formData.get('title') as string })
}
return (
<form onSubmit={handleSubmit}>
<input name="title" required />
<button type="submit" disabled={isPending}>
{isPending ? 'Adding...' : 'Add Todo'}
</button>
</form>
)
}
Why this works:
- Mutations use callbacks (
onSuccess,onError,onSettled) - queries don't invalidateQueriestriggers background refetch- Mutations don't cache by default (correct behavior)
The 7-Step Setup Process
Step 1: Install Dependencies
# Core library (required)
pnpm add @tanstack/react-query
# DevTools (highly recommended for development)
pnpm add -D @tanstack/react-query-devtools
# Optional: ESLint plugin for best practices
pnpm add -D @tanstack/eslint-plugin-query
Package roles:
@tanstack/react-query- Core React hooks and QueryClient@tanstack/react-query-devtools- Visual debugger (dev only, tree-shakeable)@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 refetch0(default): Data is immediately stale, refetches on mount/focus1000 60 5: Data fresh for 5 min, no refetch during this timeInfinity: Data never stale, manual invalidation onlygcTime: How long unused data stays in cache1000 60 5(default): 5 minutesInfinity: 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 developmentrefetchOnMount: true- Ensures fresh data when component mountsrefetchOnReconnect: 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"
/>