tanstack-query
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTanStack Query v5
TanStack Query v5
Version: @tanstack/react-query@5.90.x
Requires: React 18.0+, TypeScript 4.7+
版本:@tanstack/react-query@5.90.x
依赖要求:React 18.0+、TypeScript 4.7+
v5 New Features
v5 新特性
- useMutationState — cross-component mutation tracking without prop drilling
- Simplified optimistic updates — via from pending mutations, no cache manipulation needed
variables - throwOnError — renamed from
useErrorBoundary - networkMode — offline/PWA support (|
online|always)offlineFirst - useQueries with combine — merge parallel query results into single object
- infiniteQueryOptions — type-safe factory for infinite queries (parallel to )
queryOptions - maxPages — limit pages in cache for infinite queries (requires bi-directional pagination)
- Mutation callback signature change (v5.89+) — /
onError/onSuccessnow receive 4 params (addedonSettled)onMutateResult
- useMutationState — 无需属性透传即可跨组件跟踪变更状态
- 简化的乐观更新 — 通过待处理变更的实现,无需操作缓存
variables - throwOnError — 由重命名而来
useErrorBoundary - networkMode — 支持离线/PWA(可选值:|
online|always)offlineFirst - 带combine的useQueries — 将并行查询结果合并为单个对象
- infiniteQueryOptions — 用于无限查询的类型安全工厂(与并行)
queryOptions - maxPages — 限制无限查询缓存中的页面数量(需要双向分页支持)
- 变更回调签名变更(v5.89+) — /
onError/onSuccess现在接收4个参数(新增onSettled)onMutateResult
Quick Setup
快速设置
bash
npm install @tanstack/react-query@latest @tanstack/react-query-devtools@latesttsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 min
gcTime: 1000 * 60 * 60, // 1 hour
refetchOnWindowFocus: false,
},
},
})
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}bash
npm install @tanstack/react-query@latest @tanstack/react-query-devtools@latesttsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5分钟
gcTime: 1000 * 60 * 60, // 1小时
refetchOnWindowFocus: false,
},
},
})
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}Unified Devtools (Recommended with Multiple TanStack Libraries)
统一开发者工具(推荐搭配多个TanStack库使用)
If using Query + Router (or other TanStack libraries), use the unified shell instead of individual devtools components:
TanStackDevtoolsbash
npm install -D @tanstack/react-devtoolstsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
import { TanStackDevtools } from '@tanstack/react-devtools'
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<TanStackDevtools
config={{ position: 'bottom-right' }}
plugins={[
{ name: 'TanStack Query', render: <ReactQueryDevtoolsPanel /> },
// Add more plugins: Router, etc.
]}
/>
</QueryClientProvider>
)
}Use variants (, ) when embedding inside .
*PanelReactQueryDevtoolsPanelTanStackRouterDevtoolsPanelTanStackDevtoolstsx
import { useQuery, useMutation, useQueryClient, queryOptions } from '@tanstack/react-query'
const todosQueryOptions = queryOptions({
queryKey: ['todos'],
queryFn: async () => {
const res = await fetch('/api/todos')
if (!res.ok) throw new Error('Failed to fetch')
return res.json()
},
})
function useTodos() {
return useQuery(todosQueryOptions)
}
function useAddTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (newTodo: { title: string }) => {
const res = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodo),
})
if (!res.ok) throw new Error('Failed to add')
return res.json()
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
}如果同时使用Query和Router(或其他TanStack库),请使用统一的外壳,而非单独的开发者工具组件:
TanStackDevtoolsbash
npm install -D @tanstack/react-devtoolstsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtoolsPanel } from '@tanstack/react-query-devtools'
import { TanStackDevtools } from '@tanstack/react-devtools'
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
<TanStackDevtools
config={{ position: 'bottom-right' }}
plugins={[
{ name: 'TanStack Query', render: <ReactQueryDevtoolsPanel /> },
// 添加更多插件:Router等
]}
/>
</QueryClientProvider>
)
}在中嵌入时,请使用变体(如、)。
TanStackDevtools*PanelReactQueryDevtoolsPanelTanStackRouterDevtoolsPaneltsx
import { useQuery, useMutation, useQueryClient, queryOptions } from '@tanstack/react-query'
const todosQueryOptions = queryOptions({
queryKey: ['todos'],
queryFn: async () => {
const res = await fetch('/api/todos')
if (!res.ok) throw new Error('Failed to fetch')
return res.json()
},
})
function useTodos() {
return useQuery(todosQueryOptions)
}
function useAddTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (newTodo: { title: string }) => {
const res = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodo),
})
if (!res.ok) throw new Error('Failed to add')
return res.json()
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
}Rule Categories
规则分类
| Priority | Category | Rule File | Impact |
|---|---|---|---|
| CRITICAL | Query Keys | | Prevents cache bugs and data inconsistencies |
| CRITICAL | Caching | | Optimizes performance and data freshness |
| HIGH | Invalidation | | Ensures stale data is properly refreshed |
| HIGH | Mutations | | Ensures data integrity after writes |
| HIGH | Optimistic Updates | | Responsive UI during mutations |
| HIGH | Error Handling | | Prevents poor user experiences |
| MEDIUM | Prefetching | | Improves perceived performance |
| MEDIUM | Infinite Queries | | Prevents pagination bugs |
| MEDIUM | SSR/Hydration | | Enables proper server rendering |
| MEDIUM | Parallel Queries | | Dynamic parallel fetching |
| LOW | Performance | | Reduces unnecessary re-renders |
| LOW | Offline Support | | Enables offline-first patterns |
| 优先级 | 类别 | 规则文件 | 影响 |
|---|---|---|---|
| 关键 | 查询键 | | 避免缓存错误和数据不一致 |
| 关键 | 缓存 | | 优化性能和数据新鲜度 |
| 高 | 失效 | | 确保陈旧数据被正确刷新 |
| 高 | 变更 | | 确保写入后的数据完整性 |
| 高 | 乐观更新 | | 变更过程中保持UI响应性 |
| 高 | 错误处理 | | 避免糟糕的用户体验 |
| 中 | 预获取 | | 提升感知性能 |
| 中 | 无限查询 | | 避免分页错误 |
| 中 | SSR/水合 | | 实现正确的服务端渲染 |
| 中 | 并行查询 | | 动态并行获取数据 |
| 低 | 性能 | | 减少不必要的重渲染 |
| 低 | 离线支持 | | 实现离线优先模式 |
Critical Rules
关键规则
Always Do
必须遵循
- Object syntax for all hooks:
useQuery({ queryKey, queryFn, ...options }) - Array query keys: ,
['todos'],['todos', id]['todos', { filter }] - Throw errors in queryFn:
if (!res.ok) throw new Error('Failed') - isPending for initial loading:
if (isPending) return <Loading /> - Invalidate after mutations:
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }) - queryOptions factory: reuse across ,
useQuery,useSuspenseQueryprefetchQuery - gcTime (not cacheTime): renamed in v5
- 所有钩子使用对象语法:
useQuery({ queryKey, queryFn, ...options }) - 数组形式的查询键:、
['todos']、['todos', id]['todos', { filter }] - 在queryFn中抛出错误:
if (!res.ok) throw new Error('Failed') - 使用isPending判断初始加载状态:
if (isPending) return <Loading /> - 变更后失效查询:
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }) - 使用queryOptions工厂:在、
useQuery、useSuspenseQuery之间复用prefetchQuery - 使用gcTime(而非cacheTime):v5中已重命名
Never Do
禁止操作
- v4 array/function syntax: — removed in v5
useQuery(['todos'], fetchTodos) - Query callbacks: /
onSuccess/onErrorremoved from queries (still work in mutations) — useonSettledinsteaduseEffect - isLoading for "no data yet": meaning changed in v5 — use
isPending - enabled with useSuspenseQuery: not available — use conditional rendering
- keepPreviousData: removed — use
placeholderData: keepPreviousData - refetch() for changed parameters: include params in queryKey instead, query auto-refetches
- v4的数组/函数语法:— v5中已移除
useQuery(['todos'], fetchTodos) - 查询回调:查询中的/
onSuccess/onError已移除(变更中仍可使用)— 改用onSettleduseEffect - 使用isLoading判断"无数据"状态:v5中含义已变更 — 改用
isPending - 在useSuspenseQuery中使用enabled:该选项不可用 — 改用条件渲染
- 使用keepPreviousData:已移除 — 改用
placeholderData: keepPreviousData - 使用refetch()处理参数变更:应将参数包含在查询键中,查询会自动重新获取
v4→v5 Migration Cheatsheet
v4→v5迁移速查表
| v4 | v5 | Notes |
|---|---|---|
| | Object syntax only |
| | Renamed |
| | |
| | Import |
| | Renamed |
| Removed | Use |
| | Required for infinite queries |
| | Renamed |
| | v5.89+ added 4th param |
| v4 | v5 | 说明 |
|---|---|---|
| | 仅支持对象语法 |
| | 已重命名 |
| | |
| | 需导入 |
| | 已重命名 |
查询中的 | 已移除 | 使用 |
| | 无限查询必填 |
| | 已重命名 |
| | v5.89+新增第4个参数 |
Known Issues (v5.90.x)
已知问题(v5.90.x)
- Streaming SSR hydration mismatch — +
void prefetchQuerywith conditionaluseSuspenseQueryrender causes hydration errors. Workaround:isFetchingprefetch or don't render based onawaitfetchStatus - useQuery hydration error with prefetching — + server prefetch can mismatch
useQuerybetween server/client. UseisLoadinginsteaduseSuspenseQuery - refetchOnMount ignored for errored queries — errors are always stale. Use in addition to
retryOnMount: falserefetchOnMount: false - useMutationState types — typed as
mutation.state.variablesdue to fuzzy matching. Cast explicitly inunknowncallbackselect - invalidateQueries only refetches active queries — use to include inactive queries
refetchType: 'all' - Readonly query keys break in v5.90.8 — fixed in v5.90.9+
- 流式SSR水合不匹配 — +
void prefetchQuery结合条件useSuspenseQuery渲染会导致水合错误。解决方案:isFetching预获取或不基于await渲染fetchStatus - useQuery与预获取的水合错误 — + 服务端预获取可能导致服务端与客户端的
useQuery状态不匹配。改用isLoadinguseSuspenseQuery - refetchOnMount对错误查询无效 — 错误数据始终视为陈旧。需同时设置和
retryOnMount: falserefetchOnMount: false - useMutationState类型问题 — 被类型化为
mutation.state.variables,因模糊匹配导致。需在unknown回调中显式转换类型select - invalidateQueries仅重新获取活跃查询 — 使用以包含非活跃查询
refetchType: 'all' - 只读查询键在v5.90.8中失效 — 已在v5.90.9+中修复
Key Patterns
核心模式
tsx
// Dependent queries (B waits for A)
const { data: user } = useQuery({ queryKey: ['user', id], queryFn: () => fetchUser(id) })
const { data: posts } = useQuery({
queryKey: ['posts', user?.id],
queryFn: () => fetchPosts(user!.id),
enabled: !!user,
})
// Parallel queries
const results = useQueries({
queries: ids.map(id => ({ queryKey: ['item', id], queryFn: () => fetchItem(id) })),
combine: (results) => ({ data: results.map(r => r.data), pending: results.some(r => r.isPending) }),
})
// Prefetch on hover
const handleHover = () => queryClient.prefetchQuery({ queryKey: ['item', id], queryFn: () => fetchItem(id) })
// Infinite scroll
useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam }) => fetchPosts(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
// Query cancellation
queryFn: async ({ signal }) => {
const res = await fetch(`/api/search?q=${query}`, { signal })
return res.json()
}
// Data transformation
useQuery({ queryKey: ['todos'], queryFn: fetchTodos, select: (data) => data.filter(t => t.completed) })tsx
// 依赖查询(B等待A完成)
const { data: user } = useQuery({ queryKey: ['user', id], queryFn: () => fetchUser(id) })
const { data: posts } = useQuery({
queryKey: ['posts', user?.id],
queryFn: () => fetchPosts(user!.id),
enabled: !!user,
})
// 并行查询
const results = useQueries({
queries: ids.map(id => ({ queryKey: ['item', id], queryFn: () => fetchItem(id) })),
combine: (results) => ({ data: results.map(r => r.data), pending: results.some(r => r.isPending) }),
})
// 悬停时预获取
const handleHover = () => queryClient.prefetchQuery({ queryKey: ['item', id], queryFn: () => fetchItem(id) })
// 无限滚动
useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam }) => fetchPosts(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
// 查询取消
queryFn: async ({ signal }) => {
const res = await fetch(`/api/search?q=${query}`, { signal })
return res.json()
}
// 数据转换
useQuery({ queryKey: ['todos'], queryFn: fetchTodos, select: (data) => data.filter(t => t.completed) })