- name
- tanstack-query
- description
- TanStack Query v5 (React Query) server state management. Use for data fetching, caching, mutations, or encountering v4 migration, stale data, invalidation errors.
- metadata
- {"keywords":["TanStack Query","React Query","useQuery","useMutation","useInfiniteQuery","useSuspenseQuery","QueryClient","QueryClientProvider","data fetching","server state","caching","staleTime","gcTime","query invalidation","prefetching","optimistic updates","mutations","query keys","query functions","error boundaries","suspense","React Query DevTools","v5 migration","v4 to v5","request waterfalls","background refetching","cacheTime renamed","loading status renamed","pending status","initialPageParam required","keepPreviousData removed","placeholderData","[Truncated]"]}
- license
- MIT
# TanStack Query (React Query) v5
**Status**: Production Ready ✅
**Last Updated**: 2026-08-03
**Dependencies**: React 18.0+ (18.3+ recommended), TypeScript 4.9+ (5.x preferred)
**Latest Versions**: @tanstack/react-query@5.101.4, @tanstack/react-query-devtools@5.101.4, @tanstack/eslint-plugin-query@5.101.4
---
## Quick Start (5 Minutes)
### 1. Install Dependencies
```bash
# 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
```tsx
// 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 `staleTime` to avoid excessive refetches (default is 0)
- Use `gcTime` (not `cacheTime` - 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 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
```tsx
// 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` (not `isLoading` - 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
```tsx
// 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
- `invalidateQueries` triggers background refetch
- Mutations don't cache by default (correct behavior)
---
## The 7-Step Setup Process
### Step 1: Install Dependencies
```bash
# 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
```tsx
// 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
```tsx
// 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.
---
## Critical Rules
### Always Do
✅ **Use object syntax for all hooks**
```tsx
// v5 ONLY supports this:
useQuery({ queryKey, queryFn, ...options })
useMutation({ mutationFn, ...options })
```
✅ **Use array query keys**
```tsx
queryKey: ['todos'] // List
queryKey: ['todos', id] // Detail
queryKey: ['todos', { filter }] // Filtered
```
✅ **Configure staleTime appropriately**
```tsx
staleTime: 1000 * 60 * 5 // 5 min - prevents excessive refetches
```
✅ **Use isPending for initial loading state**
```tsx
if (isPending) return <Loading />
// isPending = no data yet AND fetching
```
✅ **Throw errors in queryFn**
```tsx
if (!response.ok) throw new Error('Failed')
```
✅ **Invalidate queries after mutations**
```tsx
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
}
```
✅ **Use queryOptions factory for reusable patterns**
```tsx
const opts = queryOptions({ queryKey, queryFn })
useQuery(opts)
useSuspenseQuery(opts)
prefetchQuery(opts)
```
✅ **Use gcTime (not cacheTime)**
```tsx
gcTime: 1000 * 60 * 60 // 1 hour
```
✅ **Know your status flags**
```tsx
isPending // no data yet, fetch in flight
isFetching // any fetch in flight (including refetch)
isRefetching // refetch specifically (data already cached)
isLoadingError // initial load failed
isPaused // networkMode paused (e.g., offline)
isFetchingNextPage // useInfiniteQuery loading more
```
### Never Do
❌ **Never use v4 array/function syntax**
```tsx
// v4 (removed in v5):
useQuery(['todos'], fetchTodos, options) // ❌
// v5 (correct):
useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) // ✅
```
❌ **Never use query callbacks (onSuccess, onError, onSettled in queries)**
```tsx
// 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**
```tsx
// 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"**
```tsx
// 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**
Auf GitHub ansehen