tanstack-query
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTanStack Query (React Query) v5
TanStack Query (React Query) v5
Status: Production Ready ✅
Last Updated: 2025-10-22
Dependencies: React 18.0+, TypeScript 4.7+ (recommended)
Latest Versions: @tanstack/react-query@5.90.5, @tanstack/react-query-devtools@5.90.2
状态:已就绪可用于生产环境 ✅
最后更新:2025-10-22
依赖:React 18.0+,TypeScript 4.7+(推荐)
最新版本:@tanstack/react-query@5.90.5,@tanstack/react-query-devtools@5.90.2
Quick Start (5 Minutes)
快速开始(5分钟)
1. Install Dependencies
1. 安装依赖
bash
npm install @tanstack/react-query@latest
npm install -D @tanstack/react-query-devtools@latestWhy 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
bash
npm install @tanstack/react-query@latest
npm install -D @tanstack/react-query-devtools@latest为什么这很重要:
- TanStack Query v5需要React 18+(使用useSyncExternalStore)
- DevTools是调试查询和突变的必备工具
- v5相对于v4有破坏性变更 - 使用最新版本获取所有修复
2. Set Up QueryClient Provider
2. 设置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 to avoid excessive refetches (default is 0)
staleTime - Use (not
gcTime- renamed in v5)cacheTime - DevTools should be inside provider
tsx
// src/main.tsx 或 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'
// 创建客户端
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5分钟
gcTime: 1000 * 60 * 60, // 1小时(原名为cacheTime)
retry: 1,
refetchOnWindowFocus: false,
},
},
})
createRoot(document.getElementById('root')!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</StrictMode>
)关键注意事项:
- 用包裹整个应用
QueryClientProvider - 配置以避免过度重新获取(默认值为0)
staleTime - 使用(而非
gcTime- v5中已重命名)cacheTime - DevTools应放在provider内部
3. Create First Query
3. 创建第一个查询
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 (not
isPending- that now means "pending AND fetching")isLoading - Always throw errors in queryFn for proper error handling
- QueryKey should be array for consistent cache keys
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('获取待办事项失败')
}
return response.json()
}
export function useTodos() {
return useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
}
// 在组件中使用:
function TodoList() {
const { data, isPending, isError, error } = useTodos()
if (isPending) return <div>加载中...</div>
if (isError) return <div>错误:{error.message}</div>
return (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}关键注意事项:
- v5要求使用对象语法:
useQuery({ queryKey, queryFn }) - 使用(而非
isPending- 现在它表示"pending且正在获取")isLoading - 始终在queryFn中抛出错误以实现正确的错误处理
- QueryKey应为数组形式以保证缓存键的一致性
4. Create First Mutation
4. 创建第一个突变
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) - queries don'tonSettled - triggers background refetch
invalidateQueries - Mutations don't cache by default (correct behavior)
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('添加待办事项失败')
return response.json()
}
export function useAddTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: addTodo,
onSuccess: () => {
// 使查询失效并重新获取待办事项
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
}
// 在组件中使用:
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 ? '添加中...' : '添加待办事项'}
</button>
</form>
)
}为什么这有效:
- 突变使用回调函数(,
onSuccess,onError)- 查询不使用onSettled - 触发后台重新获取
invalidateQueries - 突变默认不缓存(正确行为)
The 7-Step Setup Process
7步设置流程
Step 1: Install Dependencies
步骤1:安装依赖
bash
undefinedbash
undefinedCore library (required)
核心库(必需)
npm install @tanstack/react-query
npm install @tanstack/react-query
DevTools (highly recommended for development)
DevTools(开发时强烈推荐)
npm install -D @tanstack/react-query-devtools
npm install -D @tanstack/react-query-devtools
Optional: ESLint plugin for best practices
可选:用于最佳实践的ESLint插件
npm install -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 4.7+ for best type inference (optional but recommended)npm install -D @tanstack/eslint-plugin-query
**包的作用**:
- `@tanstack/react-query` - 核心React钩子和QueryClient
- `@tanstack/react-query-devtools` - 可视化调试工具(仅开发环境,可通过tree-shaking移除)
- `@tanstack/eslint-plugin-query` - 捕获常见错误
**版本要求**:
- React 18.0或更高版本(使用`useSyncExternalStore`)
- TypeScript 4.7+以获得最佳类型推断(可选但推荐)Step 2: Configure QueryClient
步骤2:配置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:
- : How long until data is considered "stale" and might refetch
staleTime- (default): Data is immediately stale, refetches on mount/focus
0 - : Data fresh for 5 min, no refetch during this time
1000 * 60 * 5 - : Data never stale, manual invalidation only
Infinity
- : How long unused data stays in cache
gcTime- (default): 5 minutes
1000 * 60 * 5 - : Never garbage collect (memory leak risk)
Infinity
When to refetch:
- - Good for frequently changing data (stock prices)
refetchOnWindowFocus: true - - Good for stable data or during development
refetchOnWindowFocus: false - - Ensures fresh data when component mounts
refetchOnMount: true - - Refetch after network reconnect
refetchOnReconnect: true
tsx
// src/lib/query-client.ts
import { QueryClient } from '@tanstack/react-query'
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
// 数据被认为是新鲜的时长(在此期间不会重新获取)
staleTime: 1000 * 60 * 5, // 5分钟
// 非活跃数据在缓存中保留到被垃圾回收的时长
gcTime: 1000 * 60 * 60, // 1小时(v5:从cacheTime重命名)
// 失败请求的重试次数(服务器端默认0次,客户端默认3次)
retry: (failureCount, error) => {
if (error instanceof Response && error.status === 404) return false
return failureCount < 3
},
// 窗口聚焦时重新获取(开发时可能会烦人)
refetchOnWindowFocus: false,
// 网络重新连接时重新获取
refetchOnReconnect: true,
// 如果数据已过期,组件挂载时重新获取
refetchOnMount: true,
},
mutations: {
// 失败时重试突变(通常不希望这样)
retry: 0,
},
},
})关键配置决策:
staleTime vs gcTime:
- :数据被认为"过期"并可能重新获取的时长
staleTime- (默认):数据立即过期,挂载/聚焦时重新获取
0 - :数据5分钟内保持新鲜,期间不会重新获取
1000 * 60 * 5 - :数据永不过期,仅手动失效
Infinity
- :未使用数据在缓存中保留的时长
gcTime- (默认):5分钟
1000 * 60 * 5 - :永不垃圾回收(有内存泄漏风险)
Infinity
何时重新获取:
- - 适用于频繁变化的数据(如股票价格)
refetchOnWindowFocus: true - - 适用于稳定数据或开发期间
refetchOnWindowFocus: false - - 确保组件挂载时获取新鲜数据
refetchOnMount: true - - 网络重新连接后重新获取
refetchOnReconnect: true
Step 3: Wrap App with Provider
步骤3:用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:
- - Collapsed by default
initialIsOpen={false} - - Where to show toggle button
buttonPosition="bottom-right" - Automatically removed in production builds (tree-shaken)
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放置:
- 必须包裹所有使用TanStack Query钩子的组件
- DevTools必须放在provider内部
- 整个应用只需一个QueryClient实例
DevTools配置:
- - 默认折叠
initialIsOpen={false} - - 切换按钮的位置
buttonPosition="bottom-right" - 生产构建中会自动移除(tree-shaken)
Step 4: Create Custom Query Hooks
步骤4:创建自定义查询钩子
Pattern: Reusable Query Hooks
tsx
// src/api/todos.ts - API functions
export type Todo = {
id: number
title: string
completed: boolean
}
export async function fetchTodos(): Promise<Todo[]> {
const response = await fetch('/api/todos')
if (!response.ok) {
throw new Error(`Failed to fetch todos: ${response.statusText}`)
}
return response.json()
}
export async function fetchTodoById(id: number): Promise<Todo> {
const response = await fetch(`/api/todos/${id}`)
if (!response.ok) {
throw new Error(`Failed to fetch todo ${id}: ${response.statusText}`)
}
return response.json()
}
// src/hooks/useTodos.ts - Query hooks
import { useQuery, queryOptions } from '@tanstack/react-query'
import { fetchTodos, fetchTodoById } from '../api/todos'
// Query options factory (v5 pattern for reusability)
export const todosQueryOptions = queryOptions({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 1000 * 60, // 1 minute
})
export function useTodos() {
return useQuery(todosQueryOptions)
}
export function useTodo(id: number) {
return useQuery({
queryKey: ['todos', id],
queryFn: () => fetchTodoById(id),
enabled: !!id, // Only fetch if id is truthy
})
}Why use queryOptions factory:
- Type inference works perfectly
- Reusable across ,
useQuery,useSuspenseQueryprefetchQuery - Consistent queryKey and queryFn everywhere
- Easier to test and maintain
Query key structure:
- - List of all todos
['todos'] - - Single todo detail
['todos', id] - - Filtered list
['todos', 'filters', { status: 'completed' }] - More specific keys are subsets (invalidating invalidates all)
['todos']
模式:可复用查询钩子
tsx
// src/api/todos.ts - API函数
export type Todo = {
id: number
title: string
completed: boolean
}
export async function fetchTodos(): Promise<Todo[]> {
const response = await fetch('/api/todos')
if (!response.ok) {
throw new Error(`获取待办事项失败:${response.statusText}`)
}
return response.json()
}
export async function fetchTodoById(id: number): Promise<Todo> {
const response = await fetch(`/api/todos/${id}`)
if (!response.ok) {
throw new Error(`获取待办事项${id}失败:${response.statusText}`)
}
return response.json()
}
// src/hooks/useTodos.ts - 查询钩子
import { useQuery, queryOptions } from '@tanstack/react-query'
import { fetchTodos, fetchTodoById } from '../api/todos'
// 查询选项工厂(v5的可复用模式)
export const todosQueryOptions = queryOptions({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 1000 * 60, // 1分钟
})
export function useTodos() {
return useQuery(todosQueryOptions)
}
export function useTodo(id: number) {
return useQuery({
queryKey: ['todos', id],
queryFn: () => fetchTodoById(id),
enabled: !!id, // 仅当id为真时才获取
})
}为什么使用queryOptions工厂:
- 类型推断完美工作
- 可在,
useQuery,useSuspenseQuery之间复用prefetchQuery - 确保所有地方的queryKey和queryFn一致
- 更易于测试和维护
查询键结构:
- - 所有待办事项列表
['todos'] - - 单个待办事项详情
['todos', id] - - 筛选后的列表
['todos', 'filters', { status: 'completed' }] - 更具体的键是子集(使失效会使所有相关键失效)
['todos']
Step 5: Implement Mutations with Optimistic Updates
步骤5:实现带乐观更新的突变
tsx
// src/hooks/useTodoMutations.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'
import type { Todo } from '../api/todos'
type AddTodoInput = {
title: string
}
type UpdateTodoInput = {
id: number
completed: boolean
}
export function useAddTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (newTodo: AddTodoInput) => {
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()
},
// Optimistic update
onMutate: async (newTodo) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['todos'] })
// Snapshot previous value
const previousTodos = queryClient.getQueryData<Todo[]>(['todos'])
// Optimistically update
queryClient.setQueryData<Todo[]>(['todos'], (old = []) => [
...old,
{ id: Date.now(), ...newTodo, completed: false },
])
// Return context with snapshot
return { previousTodos }
},
// Rollback on error
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context?.previousTodos)
},
// Always refetch after error or success
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
}
export function useUpdateTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ id, completed }: UpdateTodoInput) => {
const response = await fetch(`/api/todos/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ completed }),
})
if (!response.ok) throw new Error('Failed to update todo')
return response.json()
},
onSuccess: (updatedTodo) => {
// Update the specific todo in cache
queryClient.setQueryData<Todo>(['todos', updatedTodo.id], updatedTodo)
// Invalidate list to refetch
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
}
export function useDeleteTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (id: number) => {
const response = await fetch(`/api/todos/${id}`, { method: 'DELETE' })
if (!response.ok) throw new Error('Failed to delete todo')
},
onSuccess: (_, deletedId) => {
queryClient.setQueryData<Todo[]>(['todos'], (old = []) =>
old.filter(todo => todo.id !== deletedId)
)
},
})
}Optimistic update pattern:
- : Cancel queries, snapshot old data, update cache optimistically
onMutate - : Rollback to snapshot if mutation fails
onError - : Refetch to ensure cache matches server
onSettled
When to use:
- Immediate UI feedback for better UX
- Low-risk mutations (todo toggle, like button)
- Avoid for critical data (payments, account settings)
tsx
// src/hooks/useTodoMutations.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'
import type { Todo } from '../api/todos'
type AddTodoInput = {
title: string
}
type UpdateTodoInput = {
id: number
completed: boolean
}
export function useAddTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (newTodo: AddTodoInput) => {
const response = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodo),
})
if (!response.ok) throw new Error('添加待办事项失败')
return response.json()
},
// 乐观更新
onMutate: async (newTodo) => {
// 取消正在进行的重新获取
await queryClient.cancelQueries({ queryKey: ['todos'] })
// 快照之前的值
const previousTodos = queryClient.getQueryData<Todo[]>(['todos'])
// 乐观更新缓存
queryClient.setQueryData<Todo[]>(['todos'], (old = []) => [
...old,
{ id: Date.now(), ...newTodo, completed: false },
])
// 返回包含快照的上下文
return { previousTodos }
},
// 错误时回滚
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context?.previousTodos)
},
// 无论成功或错误都重新获取
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
}
export function useUpdateTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ id, completed }: UpdateTodoInput) => {
const response = await fetch(`/api/todos/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ completed }),
})
if (!response.ok) throw new Error('更新待办事项失败')
return response.json()
},
onSuccess: (updatedTodo) => {
// 更新缓存中的特定待办事项
queryClient.setQueryData<Todo>(['todos', updatedTodo.id], updatedTodo)
// 使列表失效以重新获取
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
}
export function useDeleteTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (id: number) => {
const response = await fetch(`/api/todos/${id}`, { method: 'DELETE' })
if (!response.ok) throw new Error('删除待办事项失败')
},
onSuccess: (_, deletedId) => {
queryClient.setQueryData<Todo[]>(['todos'], (old = []) =>
old.filter(todo => todo.id !== deletedId)
)
},
})
}乐观更新模式:
- :取消查询,快照旧数据,乐观更新缓存
onMutate - :如果突变失败,回滚到快照
onError - :重新获取以确保缓存与服务器一致
onSettled
何时使用:
- 即时UI反馈以提升用户体验
- 低风险突变(待办事项切换、点赞按钮)
- 避免用于关键数据(支付、账户设置)
Step 6: Set Up DevTools
步骤6:设置DevTools
tsx
// Already set up in main.tsx, but here are advanced options:
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
<ReactQueryDevtools
initialIsOpen={false}
buttonPosition="bottom-right"
position="bottom"
// Custom toggle button
toggleButtonProps={{
style: { marginBottom: '4rem' },
}}
// Custom panel styles
panelProps={{
style: { height: '400px' },
}}
// Only show in dev (already tree-shaken in production)
// But can add explicit check:
// {import.meta.env.DEV && <ReactQueryDevtools />}
/>DevTools features:
- View all queries and their states
- See query cache contents
- Manually refetch queries
- View mutations in flight
- Inspect query dependencies
- Export state for debugging
tsx
// 已在main.tsx中设置,以下是高级选项:
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
<ReactQueryDevtools
initialIsOpen={false}
buttonPosition="bottom-right"
position="bottom"
// 自定义切换按钮
toggleButtonProps={{
style: { marginBottom: '4rem' },
}}
// 自定义面板样式
panelProps={{
style: { height: '400px' },
}}
// 仅在开发环境显示(生产中已自动tree-shaken)
// 也可以添加显式检查:
// {import.meta.env.DEV && <ReactQueryDevtools />}
/>DevTools功能:
- 查看所有查询及其状态
- 查看查询缓存内容
- 手动重新获取查询
- 查看进行中的突变
- 检查查询依赖
- 导出状态用于调试
Step 7: Configure Error Boundaries
步骤7:配置错误边界
tsx
// src/components/ErrorBoundary.tsx
import { Component, type ReactNode } from 'react'
import { QueryErrorResetBoundary, useQueryErrorResetBoundary } from '@tanstack/react-query'
type Props = { children: ReactNode }
type State = { hasError: boolean }
class ErrorBoundaryClass extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError() {
return { hasError: true }
}
render() {
if (this.state.hasError) {
return (
<div>
<h2>Something went wrong</h2>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
)
}
return this.props.children
}
}
// Wrapper with TanStack Query error reset
export function ErrorBoundary({ children }: Props) {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundaryClass onReset={reset}>
{children}
</ErrorBoundaryClass>
)}
</QueryErrorResetBoundary>
)
}
// Usage with throwOnError option:
function useTodosWithErrorBoundary() {
return useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
throwOnError: true, // Throw errors to error boundary
})
}
// Or conditional:
function useTodosConditionalError() {
return useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
throwOnError: (error, query) => {
// Only throw server errors, handle network errors locally
return error instanceof Response && error.status >= 500
},
})
}Error handling strategies:
- Local handling: Use and
isErrorfrom queryerror - Global handling: Use error boundaries with
throwOnError - Mixed: Conditional function
throwOnError - Centralized: Use global error handlers
QueryCache
tsx
// src/components/ErrorBoundary.tsx
import { Component, type ReactNode } from 'react'
import { QueryErrorResetBoundary, useQueryErrorResetBoundary } from '@tanstack/react-query'
type Props = { children: ReactNode }
type State = { hasError: boolean }
class ErrorBoundaryClass extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError() {
return { hasError: true }
}
render() {
if (this.state.hasError) {
return (
<div>
<h2>出现错误</h2>
<button onClick={() => this.setState({ hasError: false })}>
重试
</button>
</div>
)
}
return this.props.children
}
}
// 带有TanStack Query错误重置的包装器
export function ErrorBoundary({ children }: Props) {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundaryClass onReset={reset}>
{children}
</ErrorBoundaryClass>
)}
</QueryErrorResetBoundary>
)
}
// 结合throwOnError选项使用:
function useTodosWithErrorBoundary() {
return useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
throwOnError: true, // 向错误边界抛出错误
})
}
// 或条件式:
function useTodosConditionalError() {
return useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
throwOnError: (error, query) => {
// 仅抛出服务器错误,本地处理网络错误
return error instanceof Response && error.status >= 500
},
})
}错误处理策略:
- 本地处理:使用查询返回的和
isErrorerror - 全局处理:结合使用错误边界
throwOnError - 混合方式:条件式函数
throwOnError - 集中式:使用全局错误处理程序
QueryCache
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✅ 所有钩子都使用对象语法
tsx
// v5仅支持这种方式:
useQuery({ queryKey, queryFn, ...options })
useMutation({ mutationFn, ...options })✅ 使用数组形式的查询键
tsx
queryKey: ['todos'] // 列表
queryKey: ['todos', id] // 详情
queryKey: ['todos', { filter }] // 筛选后的列表✅ 适当配置staleTime
tsx
staleTime: 1000 * 60 * 5 // 5分钟 - 防止过度重新获取✅ 使用isPending表示初始加载状态
tsx
if (isPending) return <加载中 />
// isPending = 尚无数据且正在获取✅ 在queryFn中抛出错误
tsx
if (!response.ok) throw new Error('失败')✅ 突变后使查询失效
tsx
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
}✅ 使用queryOptions工厂实现可复用模式
tsx
const opts = queryOptions({ queryKey, queryFn })
useQuery(opts)
useSuspenseQuery(opts)
prefetchQuery(opts)✅ 使用gcTime(而非cacheTime)
tsx
gcTime: 1000 * 60 * 60 // 1小时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
tsx
// v5 requires this:
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam }) => fetchProjects(pageParam),
initialPageParam: 0, // ✅ Required in v5
getNextPageParam: (lastPage) => lastPage.nextCursor,
})❌ Never use enabled with useSuspenseQuery
tsx
// Not allowed:
useSuspenseQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
enabled: !!id, // ❌ Not available with suspense
})
// Use conditional rendering instead:
{id && <TodoComponent id={id} />}❌ 永远不要使用v4的数组/函数语法
tsx
// v4(v5中已移除):
useQuery(['todos'], fetchTodos, options) // ❌
// v5(正确方式):
useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) // ✅❌ 永远不要使用查询回调(查询中的onSuccess、onError、onSettled)
tsx
// v5已从查询中移除这些:
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
onSuccess: (data) => {}, // ❌ v5中已移除
})
// 改用useEffect:
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
if (data) {
// 执行操作
}
}, [data])
// 或使用突变回调(仍然支持):
useMutation({
mutationFn: addTodo,
onSuccess: () => {}, // ✅ 突变中仍然有效
})❌ 永远不要使用已废弃的选项
tsx
// v5中已废弃:
cacheTime: 1000 // ❌ 改用gcTime
isLoading: true // ❌ 含义已变更,使用isPending
keepPreviousData: true // ❌ 改用placeholderData
onSuccess: () => {} // ❌ 已从查询中移除
useErrorBoundary: true // ❌ 改用throwOnError❌ 永远不要假设isLoading表示"尚无数据"
tsx
// v5已变更:
isLoading = isPending && isFetching // ❌ 现在表示"pending且正在获取"
isPending = 尚无数据 // ✅ 用于初始加载❌ 永远不要忘记无限查询的initialPageParam
tsx
// v5要求必须设置:
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam }) => fetchProjects(pageParam),
initialPageParam: 0, // ✅ v5中必需
getNextPageParam: (lastPage) => lastPage.nextCursor,
})❌ 永远不要在useSuspenseQuery中使用enabled
tsx
// 不允许:
useSuspenseQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
enabled: !!id, // ❌ Suspense中不可用
})
// 改用条件渲染:
{id && <TodoComponent id={id} />}Known Issues Prevention
已知问题预防
This skill prevents 8 documented issues from v5 migration and common mistakes:
本技能可预防v5迁移和常见错误中的8个文档化问题:
Issue #1: Object Syntax Required
问题#1:必须使用对象语法
Error: or type errors
Source: v5 Migration Guide
Why It Happens: v5 removed all function overloads, only object syntax works
Prevention: Always use
useQuery is not a functionuseQuery({ queryKey, queryFn, ...options })Before (v4):
tsx
useQuery(['todos'], fetchTodos, { staleTime: 5000 })After (v5):
tsx
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5000
})错误:或类型错误
来源:v5迁移指南
原因:v5移除了所有函数重载,仅支持对象语法
预防:始终使用
useQuery is not a functionuseQuery({ queryKey, queryFn, ...options })之前(v4):
tsx
useQuery(['todos'], fetchTodos, { staleTime: 5000 })之后(v5):
tsx
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5000
})Issue #2: Query Callbacks Removed
问题#2:查询回调已移除
Error: Callbacks don't run, TypeScript errors
Source: v5 Breaking Changes
Why It Happens: onSuccess, onError, onSettled removed from queries (still work in mutations)
Prevention: Use for side effects, or move logic to mutation callbacks
useEffectBefore (v4):
tsx
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
onSuccess: (data) => {
console.log('Todos loaded:', data)
},
})After (v5):
tsx
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
if (data) {
console.log('Todos loaded:', data)
}
}, [data])错误:回调不执行,TypeScript错误
来源:v5破坏性变更
原因:onSuccess、onError、onSettled已从查询中移除(突变中仍然可用)
预防:使用处理副作用,或逻辑移至突变回调
useEffect之前(v4):
tsx
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
onSuccess: (data) => {
console.log('待办事项已加载:', data)
},
})之后(v5):
tsx
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
if (data) {
console.log('待办事项已加载:', data)
}
}, [data])Issue #3: Status Loading → Pending
问题#3:状态Loading → Pending
Error: UI shows wrong loading state
Source: v5 Migration: isLoading renamed
Why It Happens: renamed to , meaning changed
Prevention: Use for initial load, for "pending AND fetching"
status: 'loading'status: 'pending'isLoadingisPendingisLoadingBefore (v4):
tsx
const { data, isLoading } = useQuery(...)
if (isLoading) return <div>Loading...</div>After (v5):
tsx
const { data, isPending, isLoading } = useQuery(...)
if (isPending) return <div>Loading...</div>
// isLoading = isPending && isFetching (fetching for first time)错误:UI显示错误的加载状态
来源:v5迁移:isLoading重命名
原因:重命名为,的含义已变更
预防:使用表示初始加载,表示"pending且正在获取"
status: 'loading'status: 'pending'isLoadingisPendingisLoading之前(v4):
tsx
const { data, isLoading } = useQuery(...)
if (isLoading) return <div>加载中...</div>之后(v5):
tsx
const { data, isPending, isLoading } = useQuery(...)
if (isPending) return <div>加载中...</div>
// isLoading = isPending && isFetching(首次获取时)Issue #4: cacheTime → gcTime
问题#4:cacheTime → gcTime
Error:
Source: v5 Migration: gcTime
Why It Happens: Renamed to better reflect "garbage collection time"
Prevention: Use instead of
cacheTime is not a valid optiongcTimecacheTimeBefore (v4):
tsx
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
cacheTime: 1000 * 60 * 60,
})After (v5):
tsx
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
gcTime: 1000 * 60 * 60,
})之前(v4):
tsx
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
cacheTime: 1000 * 60 * 60,
})之后(v5):
tsx
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
gcTime: 1000 * 60 * 60,
})Issue #5: useSuspenseQuery + enabled
问题#5:useSuspenseQuery + enabled
Error: Type error, enabled option not available
Source: GitHub Discussion #6206
Why It Happens: Suspense guarantees data is available, can't conditionally disable
Prevention: Use conditional rendering instead of option
enabledBefore (v4/incorrect):
tsx
useSuspenseQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
enabled: !!id, // ❌ Not allowed
})After (v5/correct):
tsx
// Conditional rendering:
{id ? (
<TodoComponent id={id} />
) : (
<div>No ID selected</div>
)}
// Inside TodoComponent:
function TodoComponent({ id }: { id: number }) {
const { data } = useSuspenseQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
// No enabled option needed
})
return <div>{data.title}</div>
}错误:类型错误,enabled选项不可用
来源:GitHub讨论#6206
原因:Suspense保证数据可用,无法条件式禁用
预防:使用条件渲染替代选项
enabled之前(v4/错误方式):
tsx
useSuspenseQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
enabled: !!id, // ❌ 不允许
})之后(v5/正确方式):
tsx
// 条件渲染:
{id ? (
<TodoComponent id={id} />
) : (
<div>未选择ID</div>
)}
// 在TodoComponent内部:
function TodoComponent({ id }: { id: number }) {
const { data } = useSuspenseQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
// 无需enabled选项
})
return <div>{data.title}</div>
}Issue #6: initialPageParam Required
问题#6:initialPageParam必需
Error: type error
Source: v5 Migration: Infinite Queries
Why It Happens: v4 passed as first pageParam, v5 requires explicit value
Prevention: Always specify for infinite queries
initialPageParam is requiredundefinedinitialPageParamBefore (v4):
tsx
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam = 0 }) => fetchProjects(pageParam),
getNextPageParam: (lastPage) => lastPage.nextCursor,
})After (v5):
tsx
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam }) => fetchProjects(pageParam),
initialPageParam: 0, // ✅ Required
getNextPageParam: (lastPage) => lastPage.nextCursor,
})错误:类型错误
来源:v5迁移:无限查询
原因:v4传递作为第一个pageParam,v5要求显式值
预防:无限查询始终指定
initialPageParam is requiredundefinedinitialPageParam之前(v4):
tsx
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam = 0 }) => fetchProjects(pageParam),
getNextPageParam: (lastPage) => lastPage.nextCursor,
})之后(v5):
tsx
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam }) => fetchProjects(pageParam),
initialPageParam: 0, // ✅ 必需
getNextPageParam: (lastPage) => lastPage.nextCursor,
})Issue #7: keepPreviousData Removed
问题#7:keepPreviousData已移除
Error:
Source: v5 Migration: placeholderData
Why It Happens: Replaced with more flexible function
Prevention: Use helper
keepPreviousData is not a valid optionplaceholderDataplaceholderData: keepPreviousDataBefore (v4):
tsx
useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
keepPreviousData: true,
})After (v5):
tsx
import { keepPreviousData } from '@tanstack/react-query'
useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
placeholderData: keepPreviousData,
})错误:
来源:v5迁移:placeholderData
原因:被更灵活的函数替代
预防:使用助手
keepPreviousData is not a valid optionplaceholderDataplaceholderData: keepPreviousData之前(v4):
tsx
useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
keepPreviousData: true,
})之后(v5):
tsx
import { keepPreviousData } from '@tanstack/react-query'
useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
placeholderData: keepPreviousData,
})Issue #8: TypeScript Error Type Default
问题#8:TypeScript错误类型默认值
Error: Type errors with error handling
Source: v5 Migration: Error Types
Why It Happens: v4 used , v5 defaults to type
Prevention: If throwing non-Error types, specify error type explicitly
unknownErrorBefore (v4 - error was unknown):
tsx
const { error } = useQuery({
queryKey: ['data'],
queryFn: async () => {
if (Math.random() > 0.5) throw 'custom error string'
return data
},
})
// error: unknownAfter (v5 - specify custom error type):
tsx
const { error } = useQuery<DataType, string>({
queryKey: ['data'],
queryFn: async () => {
if (Math.random() > 0.5) throw 'custom error string'
return data
},
})
// error: string | null
// Or better: always throw Error objects
const { error } = useQuery({
queryKey: ['data'],
queryFn: async () => {
if (Math.random() > 0.5) throw new Error('custom error')
return data
},
})
// error: Error | null (default)之前(v4 - error为unknown):
tsx
const { error } = useQuery({
queryKey: ['data'],
queryFn: async () => {
if (Math.random() > 0.5) throw '自定义错误字符串'
return data
},
})
// error: unknown之后(v5 - 指定自定义错误类型):
tsx
const { error } = useQuery<DataType, string>({
queryKey: ['data'],
queryFn: async () => {
if (Math.random() > 0.5) throw '自定义错误字符串'
return data
},
})
// error: string | null
// 更好的方式:始终抛出Error对象
const { error } = useQuery({
queryKey: ['data'],
queryFn: async () => {
if (Math.random() > 0.5) throw new Error('自定义错误')
return data
},
})
// error: Error | null(默认)Configuration Files Reference
配置文件参考
package.json (Full Example)
package.json(完整示例)
json
{
"name": "my-app",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"@tanstack/react-query": "^5.90.5"
},
"devDependencies": {
"@tanstack/react-query-devtools": "^5.90.2",
"@tanstack/eslint-plugin-query": "^5.90.2",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.6.3",
"vite": "^6.0.1"
}
}Why these versions:
- React 18.3.1 - Required for useSyncExternalStore
- TanStack Query 5.90.5 - Latest stable with all v5 fixes
- DevTools 5.90.2 - Matched to query version
- TypeScript 5.6.3 - Best type inference for query types
json
{
"name": "my-app",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"@tanstack/react-query": "^5.90.5"
},
"devDependencies": {
"@tanstack/react-query-devtools": "^5.90.2",
"@tanstack/eslint-plugin-query": "^5.90.2",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.6.3",
"vite": "^6.0.1"
}
}为什么选择这些版本:
- React 18.3.1 - useSyncExternalStore所需
- TanStack Query 5.90.5 - 最新稳定版,包含所有v5修复
- DevTools 5.90.2 - 与查询版本匹配
- TypeScript 5.6.3 - 对查询类型的最佳推断
tsconfig.json (TypeScript Configuration)
tsconfig.json(TypeScript配置)
json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* TanStack Query specific */
"esModuleInterop": true,
"resolveJsonModule": true
},
"include": ["src"]
}json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* 打包器模式 */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* 代码检查 */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* TanStack Query特定配置 */
"esModuleInterop": true,
"resolveJsonModule": true
},
"include": ["src"]
}.eslintrc.cjs (ESLint Configuration with TanStack Query Plugin)
.eslintrc.cjs(带有TanStack Query插件的ESLint配置)
javascript
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'plugin:@tanstack/eslint-plugin-query/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh', '@tanstack/query'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}ESLint plugin catches:
- Query keys as references instead of inline
- Missing queryFn
- Using v4 patterns in v5
- Incorrect dependencies in useEffect
javascript
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'plugin:@tanstack/eslint-plugin-query/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh', '@tanstack/query'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}ESLint插件捕获的问题:
- 查询键使用引用而非内联
- 缺少queryFn
- 在v5中使用v4模式
- useEffect中的依赖不正确
Common Patterns
常见模式
Pattern 1: Dependent Queries
模式1:依赖查询
tsx
// Fetch user, then fetch user's posts
function UserPosts({ userId }: { userId: number }) {
const { data: user } = useQuery({
queryKey: ['users', userId],
queryFn: () => fetchUser(userId),
})
const { data: posts } = useQuery({
queryKey: ['users', userId, 'posts'],
queryFn: () => fetchUserPosts(userId),
enabled: !!user, // Only fetch posts after user is loaded
})
if (!user) return <div>Loading user...</div>
if (!posts) return <div>Loading posts...</div>
return (
<div>
<h1>{user.name}</h1>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
)
}When to use: Query B depends on data from Query A
tsx
// 获取用户,然后获取用户的帖子
function UserPosts({ userId }: { userId: number }) {
const { data: user } = useQuery({
queryKey: ['users', userId],
queryFn: () => fetchUser(userId),
})
const { data: posts } = useQuery({
queryKey: ['users', userId, 'posts'],
queryFn: () => fetchUserPosts(userId),
enabled: !!user, // 仅在用户加载后获取帖子
})
if (!user) return <div>加载用户中...</div>
if (!posts) return <div>加载帖子中...</div>
return (
<div>
<h1>{user.name}</h1>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
)
}适用场景:查询B依赖于查询A的数据
Pattern 2: Parallel Queries with useQueries
模式2:使用useQueries的并行查询
tsx
// Fetch multiple todos in parallel
function TodoDetails({ ids }: { ids: number[] }) {
const results = useQueries({
queries: ids.map(id => ({
queryKey: ['todos', id],
queryFn: () => fetchTodo(id),
})),
})
const isLoading = results.some(result => result.isPending)
const isError = results.some(result => result.isError)
if (isLoading) return <div>Loading...</div>
if (isError) return <div>Error loading todos</div>
return (
<ul>
{results.map((result, i) => (
<li key={ids[i]}>{result.data?.title}</li>
))}
</ul>
)
}When to use: Fetch multiple independent queries in parallel
tsx
// 并行获取多个待办事项
function TodoDetails({ ids }: { ids: number[] }) {
const results = useQueries({
queries: ids.map(id => ({
queryKey: ['todos', id],
queryFn: () => fetchTodo(id),
})),
})
const isLoading = results.some(result => result.isPending)
const isError = results.some(result => result.isError)
if (isLoading) return <div>加载中...</div>
if (isError) return <div>加载待办事项出错</div>
return (
<ul>
{results.map((result, i) => (
<li key={ids[i]}>{result.data?.title}</li>
))}
</ul>
)
}适用场景:并行获取多个独立查询
Pattern 3: Prefetching
模式3:预获取
tsx
import { useQueryClient } from '@tanstack/react-query'
import { todosQueryOptions } from './hooks/useTodos'
function TodoListWithPrefetch() {
const queryClient = useQueryClient()
const { data: todos } = useTodos()
const prefetchTodo = (id: number) => {
queryClient.prefetchQuery({
queryKey: ['todos', id],
queryFn: () => fetchTodo(id),
staleTime: 1000 * 60 * 5, // 5 minutes
})
}
return (
<ul>
{todos?.map(todo => (
<li
key={todo.id}
onMouseEnter={() => prefetchTodo(todo.id)}
>
<Link to={`/todos/${todo.id}`}>{todo.title}</Link>
</li>
))}
</ul>
)
}When to use: Preload data before user navigates (on hover, on mount)
tsx
import { useQueryClient } from '@tanstack/react-query'
import { todosQueryOptions } from './hooks/useTodos'
function TodoListWithPrefetch() {
const queryClient = useQueryClient()
const { data: todos } = useTodos()
const prefetchTodo = (id: number) => {
queryClient.prefetchQuery({
queryKey: ['todos', id],
queryFn: () => fetchTodo(id),
staleTime: 1000 * 60 * 5, // 5分钟
})
}
return (
<ul>
{todos?.map(todo => (
<li
key={todo.id}
onMouseEnter={() => prefetchTodo(todo.id)}
>
<Link to={`/todos/${todo.id}`}>{todo.title}</Link>
</li>
))}
</ul>
)
}适用场景:在用户导航前预加载数据(悬停时、挂载时)
Pattern 4: Infinite Scroll
模式4:无限滚动
tsx
import { useInfiniteQuery } from '@tanstack/react-query'
import { useEffect, useRef } from 'react'
type Page = {
data: Todo[]
nextCursor: number | null
}
async function fetchTodosPage({ pageParam }: { pageParam: number }): Promise<Page> {
const response = await fetch(`/api/todos?cursor=${pageParam}&limit=20`)
return response.json()
}
function InfiniteTodoList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['todos', 'infinite'],
queryFn: fetchTodosPage,
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
const loadMoreRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage) {
fetchNextPage()
}
},
{ threshold: 0.1 }
)
if (loadMoreRef.current) {
observer.observe(loadMoreRef.current)
}
return () => observer.disconnect()
}, [fetchNextPage, hasNextPage])
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.data.map(todo => (
<div key={todo.id}>{todo.title}</div>
))}
</div>
))}
<div ref={loadMoreRef}>
{isFetchingNextPage && <div>Loading more...</div>}
</div>
</div>
)
}When to use: Paginated lists with infinite scroll
tsx
import { useInfiniteQuery } from '@tanstack/react-query'
import { useEffect, useRef } from 'react'
type Page = {
data: Todo[]
nextCursor: number | null
}
async function fetchTodosPage({ pageParam }: { pageParam: number }): Promise<Page> {
const response = await fetch(`/api/todos?cursor=${pageParam}&limit=20`)
return response.json()
}
function InfiniteTodoList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['todos', 'infinite'],
queryFn: fetchTodosPage,
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
const loadMoreRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage) {
fetchNextPage()
}
},
{ threshold: 0.1 }
)
if (loadMoreRef.current) {
observer.observe(loadMoreRef.current)
}
return () => observer.disconnect()
}, [fetchNextPage, hasNextPage])
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.data.map(todo => (
<div key={todo.id}>{todo.title}</div>
))}
</div>
))}
<div ref={loadMoreRef}>
{isFetchingNextPage && <div>加载更多...</div>}
</div>
</div>
)
}适用场景:带无限滚动的分页列表
Pattern 5: Query Cancellation
模式5:查询取消
tsx
function SearchTodos() {
const [search, setSearch] = useState('')
const { data } = useQuery({
queryKey: ['todos', 'search', search],
queryFn: async ({ signal }) => {
const response = await fetch(`/api/todos?q=${search}`, { signal })
return response.json()
},
enabled: search.length > 2, // Only search if 3+ characters
})
return (
<div>
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search todos..."
/>
{data && (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)}
</div>
)
}How it works:
- When queryKey changes, previous query is automatically cancelled
- Pass to fetch for proper cleanup
signal - Browser aborts pending fetch requests
tsx
function SearchTodos() {
const [search, setSearch] = useState('')
const { data } = useQuery({
queryKey: ['todos', 'search', search],
queryFn: async ({ signal }) => {
const response = await fetch(`/api/todos?q=${search}`, { signal })
return response.json()
},
enabled: search.length > 2, // 仅当输入3个以上字符时搜索
})
return (
<div>
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="搜索待办事项..."
/>
{data && (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)}
</div>
)
}工作原理:
- 当queryKey变更时,之前的查询会自动取消
- 将传递给fetch以正确清理
signal - 浏览器中止待处理的fetch请求
Using Bundled Resources
使用捆绑资源
Templates (templates/)
模板(templates/)
Complete, copy-ready code examples:
- - Dependencies with exact versions
package.json - - QueryClient setup with best practices
query-client-config.ts - - App wrapper with QueryClientProvider
provider-setup.tsx - - Basic useQuery hook pattern
use-query-basic.tsx - - Basic useMutation hook
use-mutation-basic.tsx - - Optimistic update pattern
use-mutation-optimistic.tsx - - Infinite scroll pattern
use-infinite-query.tsx - - Reusable query hooks with queryOptions
custom-hooks-pattern.tsx - - Error boundary with query reset
error-boundary.tsx - - DevTools configuration
devtools-setup.tsx
Example Usage:
bash
undefined完整的、可直接复制使用的代码示例:
- - 带有精确版本的依赖
package.json - - 带有最佳实践的QueryClient设置
query-client-config.ts - - 带有QueryClientProvider的应用包装器
provider-setup.tsx - - 基本useQuery钩子模式
use-query-basic.tsx - - 基本useMutation钩子
use-mutation-basic.tsx - - 乐观更新模式
use-mutation-optimistic.tsx - - 无限滚动模式
use-infinite-query.tsx - - 带有queryOptions的可复用查询钩子
custom-hooks-pattern.tsx - - 带有查询重置的错误边界
error-boundary.tsx - - DevTools配置
devtools-setup.tsx
使用示例:
bash
undefinedCopy query client config
复制查询客户端配置
cp ~/.claude/skills/tanstack-query/templates/query-client-config.ts src/lib/
cp ~/.claude/skills/tanstack-query/templates/query-client-config.ts src/lib/
Copy provider setup
复制provider设置
cp ~/.claude/skills/tanstack-query/templates/provider-setup.tsx src/main.tsx
undefinedcp ~/.claude/skills/tanstack-query/templates/provider-setup.tsx src/main.tsx
undefinedReferences (references/)
参考资料(references/)
Deep-dive documentation loaded when needed:
- - Complete v4 → v5 migration guide
v4-to-v5-migration.md - - Request waterfalls, caching strategies, performance
best-practices.md - - Reusable queries, optimistic updates, infinite scroll
common-patterns.md - - Type safety, generics, type inference
typescript-patterns.md - - Testing with MSW, React Testing Library
testing.md - - All 8+ errors with solutions
top-errors.md
When Claude should load these:
- - When migrating existing React Query v4 project
v4-to-v5-migration.md - - When optimizing performance or avoiding waterfalls
best-practices.md - - When implementing specific features (infinite scroll, etc.)
common-patterns.md - - When dealing with TypeScript errors or type inference
typescript-patterns.md - - When writing tests for components using TanStack Query
testing.md - - When encountering errors not covered in main SKILL.md
top-errors.md
需要时加载的深度文档:
- - 完整的v4 → v5迁移指南
v4-to-v5-migration.md - - 请求瀑布、缓存策略、性能
best-practices.md - - 可复用查询、乐观更新、无限滚动
common-patterns.md - - 类型安全、泛型、类型推断
typescript-patterns.md - - 使用MSW、React Testing Library进行测试
testing.md - - 所有8+错误及解决方案
top-errors.md
Claude应加载这些资料的场景:
- - 迁移现有React Query v4项目时
v4-to-v5-migration.md - - 优化性能或避免请求瀑布时
best-practices.md - - 实现特定功能(如无限滚动)时
common-patterns.md - - 处理TypeScript错误或类型推断时
typescript-patterns.md - - 为使用TanStack Query的组件编写测试时
testing.md - - 遇到主SKILL.md中未涵盖的错误时
top-errors.md
Advanced Topics
高级主题
Data Transformations with select
使用select进行数据转换
tsx
// 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)
tsx
// 仅订阅数据的特定部分
function TodoCount() {
const { data: count } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (data) => data.length, // 仅当计数变化时重新渲染
})
return <div>待办事项总数:{count}</div>
}
// 转换数据形状
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>
)
}优势:
- 组件仅在所选数据变化时重新渲染
- 减少内存使用(组件状态中存储的数据更少)
- 保持查询缓存不变(其他组件获取完整数据)
Request Waterfalls (Anti-Pattern)
请求瀑布(反模式)
tsx
// ❌ 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!
}tsx
// ❌ 错误:顺序瀑布
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,
})
// 每个查询都等待前一个 = 速度慢!
}
// ✅ 正确:尽可能并行获取
function GoodUserProfile({ userId }: { userId: number }) {
const { data: user } = useQuery({
queryKey: ['users', userId],
queryFn: () => fetchUser(userId),
})
// 并行获取帖子和评论
const { data: posts } = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchPosts(userId), // 不等待用户数据
})
const { data: comments } = useQuery({
queryKey: ['comments', userId],
queryFn: () => fetchUserComments(userId), // 不等待帖子数据
})
// 3个查询并行运行 = 速度快!
}Server State vs Client State
服务端状态 vs 客户端状态
tsx
// ❌ 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)
tsx
// ❌ 不要将TanStack Query用于仅客户端状态
const { data: isModalOpen, setData: setIsModalOpen } = useMutation(...)
// ✅ 使用useState管理客户端状态
const [isModalOpen, setIsModalOpen] = useState(false)
// ✅ 使用TanStack Query管理服务端状态
const { data: todos } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})经验法则:
- 服务端状态:使用TanStack Query(来自API的数据)
- 客户端状态:使用useState/useReducer(本地UI状态)
- 全局客户端状态:使用Zustand/Context(主题、认证令牌)
Dependencies
依赖
Required:
- - Core library
@tanstack/react-query@5.90.5 - - Uses useSyncExternalStore hook
react@18.0.0+ - - React DOM renderer
react-dom@18.0.0+
Recommended:
- - Visual debugger (dev only)
@tanstack/react-query-devtools@5.90.2 - - ESLint rules for best practices
@tanstack/eslint-plugin-query@5.90.2 - - For type safety and inference
typescript@4.7.0+
Optional:
- - Persist cache to localStorage
@tanstack/query-sync-storage-persister - - Persist to AsyncStorage (React Native)
@tanstack/query-async-storage-persister
必需:
- - 核心库
@tanstack/react-query@5.90.5 - - 使用useSyncExternalStore钩子
react@18.0.0+ - - React DOM渲染器
react-dom@18.0.0+
推荐:
- - 可视化调试工具(仅开发环境)
@tanstack/react-query-devtools@5.90.2 - - 用于最佳实践的ESLint规则
@tanstack/eslint-plugin-query@5.90.2 - - 用于类型安全和推断
typescript@4.7.0+
可选:
- - 将缓存持久化到localStorage
@tanstack/query-sync-storage-persister - - 持久化到AsyncStorage(React Native)
@tanstack/query-async-storage-persister
Official Documentation
官方文档
- TanStack Query Docs: https://tanstack.com/query/latest
- React Integration: https://tanstack.com/query/latest/docs/framework/react/overview
- v5 Migration Guide: https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5
- API Reference: https://tanstack.com/query/latest/docs/framework/react/reference/useQuery
- Context7 Library ID:
/websites/tanstack_query - GitHub Repository: https://github.com/TanStack/query
- Discord Community: https://tlinz.com/discord
- TanStack Query文档:https://tanstack.com/query/latest
- React集成:https://tanstack.com/query/latest/docs/framework/react/overview
- v5迁移指南:https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5
- API参考:https://tanstack.com/query/latest/docs/framework/react/reference/useQuery
- Context7库ID:
/websites/tanstack_query - GitHub仓库:https://github.com/TanStack/query
- Discord社区:https://tlinz.com/discord
Package Versions (Verified 2025-10-22)
包版本(2025-10-22已验证)
json
{
"dependencies": {
"@tanstack/react-query": "^5.90.5"
},
"devDependencies": {
"@tanstack/react-query-devtools": "^5.90.2",
"@tanstack/eslint-plugin-query": "^5.90.2"
}
}Verification:
- → 5.90.5
npm view @tanstack/react-query version - → 5.90.2
npm view @tanstack/react-query-devtools version - Last checked: 2025-10-22
json
{
"dependencies": {
"@tanstack/react-query": "^5.90.5"
},
"devDependencies": {
"@tanstack/react-query-devtools": "^5.90.2",
"@tanstack/eslint-plugin-query": "^5.90.2"
}
}验证:
- → 5.90.5
npm view @tanstack/react-query version - → 5.90.2
npm view @tanstack/react-query-devtools version - 最后检查:2025-10-22
Production Example
生产示例
This skill is based on production patterns used in:
- Build Time: ~6 hours research + development
- Errors Prevented: 8 (all documented v5 migration issues)
- Token Efficiency: ~65% savings vs manual setup
- Validation: ✅ All patterns tested with TypeScript strict mode
本技能基于以下生产模式:
- 构建时间:约6小时研究+开发
- 预防的错误:8个(所有文档化的v5迁移问题)
- 令牌效率:比手动设置节省约65%
- 验证:✅ 所有模式均通过TypeScript严格模式测试
Troubleshooting
故障排除
Problem: "useQuery is not a function" or type errors
问题:"useQuery is not a function"或类型错误
Solution: Ensure you're using v5 object syntax:
tsx
// ✅ Correct:
useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
// ❌ Wrong (v4 syntax):
useQuery(['todos'], fetchTodos)解决方案:确保使用v5对象语法:
tsx
// ✅ 正确:
useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
// ❌ 错误(v4语法):
useQuery(['todos'], fetchTodos)Problem: Callbacks (onSuccess, onError) not working on queries
问题:查询上的回调(onSuccess, onError)不工作
Solution: Removed in v5. Use or move to mutations:
useEffecttsx
// ✅ For queries:
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
if (data) {
// Handle success
}
}, [data])
// ✅ For mutations (still work):
useMutation({
mutationFn: addTodo,
onSuccess: () => { /* ... */ },
})解决方案:v5中已移除。使用或移至突变:
useEffecttsx
// ✅ 对于查询:
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
if (data) {
// 处理成功
}
}, [data])
// ✅ 对于突变(仍然有效):
useMutation({
mutationFn: addTodo,
onSuccess: () => { /* ... */ },
})Problem: isLoading always false even during initial load
问题:isLoading始终为false,即使在初始加载时
Solution: Use instead:
isPendingtsx
const { isPending, isLoading, isFetching } = useQuery(...)
// isPending = no data yet
// isLoading = isPending && isFetching
// isFetching = any fetch in progress解决方案:改用:
isPendingtsx
const { isPending, isLoading, isFetching } = useQuery(...)
// isPending = 尚无数据
// isLoading = isPending && isFetching
// isFetching = 任何正在进行的获取Problem: cacheTime option not recognized
问题:cacheTime选项不被识别
Solution: Renamed to in v5:
gcTimetsx
gcTime: 1000 * 60 * 60 // 1 hour解决方案:v5中已重命名为:
gcTimetsx
gcTime: 1000 * 60 * 60 // 1小时Problem: useSuspenseQuery with enabled option gives type error
问题:useSuspenseQuery结合enabled选项出现类型错误
Solution: not available with suspense. Use conditional rendering:
enabledtsx
{id && <TodoComponent id={id} />}解决方案:suspense中不支持enabled。使用条件渲染:
tsx
{id && <TodoComponent id={id} />}Problem: Data not refetching after mutation
问题:突变后数据未重新获取
Solution: Invalidate queries in :
onSuccesstsx
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
}解决方案:在中使查询失效:
onSuccesstsx
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
}Problem: Infinite query requires initialPageParam
问题:无限查询需要initialPageParam
Solution: Always provide in v5:
initialPageParamtsx
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam }) => fetchProjects(pageParam),
initialPageParam: 0, // Required
getNextPageParam: (lastPage) => lastPage.nextCursor,
})解决方案:v5中始终提供:
initialPageParamtsx
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam }) => fetchProjects(pageParam),
initialPageParam: 0, // 必需
getNextPageParam: (lastPage) => lastPage.nextCursor,
})Problem: keepPreviousData not working
问题:keepPreviousData不工作
Solution: Replaced with :
placeholderDatatsx
import { keepPreviousData } from '@tanstack/react-query'
useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
placeholderData: keepPreviousData,
})解决方案:已被替代:
placeholderDatatsx
import { keepPreviousData } from '@tanstack/react-query'
useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
placeholderData: keepPreviousData,
})Complete Setup Checklist
完整设置检查清单
Use this checklist to verify your setup:
- Installed @tanstack/react-query@5.90.5+
- Installed @tanstack/react-query-devtools (dev dependency)
- Created QueryClient with configured defaults
- Wrapped app with QueryClientProvider
- Added ReactQueryDevtools component
- Created first query using object syntax
- Tested isPending and error states
- Created first mutation with onSuccess handler
- Set up query invalidation after mutations
- Configured staleTime and gcTime appropriately
- Using array queryKey consistently
- Throwing errors in queryFn
- No v4 syntax (function overloads)
- No query callbacks (onSuccess, onError on queries)
- Using isPending (not isLoading) for initial load
- DevTools working in development
- TypeScript types working correctly
- Production build succeeds
Questions? Issues?
- Check for complete error solutions
references/top-errors.md - Verify all steps in the setup process
- Check official docs: https://tanstack.com/query/latest
- Ensure using v5 syntax (object syntax, gcTime, isPending)
- Join Discord: https://tlinz.com/discord
使用此清单验证你的设置:
- 已安装@tanstack/react-query@5.90.5+
- 已安装@tanstack/react-query-devtools(开发依赖)
- 创建了带有默认配置的QueryClient
- 用QueryClientProvider包裹了应用
- 添加了ReactQueryDevtools组件
- 使用对象语法创建了第一个查询
- 测试了isPending和错误状态
- 创建了带有onSuccess处理程序的第一个突变
- 设置了突变后的查询失效
- 适当配置了staleTime和gcTime
- 一致使用数组形式的queryKey
- 在queryFn中抛出错误
- 未使用v4语法(函数重载)
- 未使用查询回调(查询上的onSuccess, onError)
- 使用isPending(而非isLoading)表示初始加载
- DevTools在开发环境中正常工作
- TypeScript类型正常工作
- 生产构建成功
有问题?遇到错误?
- 查看获取完整的错误解决方案
references/top-errors.md - 验证设置流程中的所有步骤
- 查看官方文档:https://tanstack.com/query/latest
- 确保使用v5语法(对象语法、gcTime、isPending)
- 加入Discord:https://tlinz.com/discord