tanstack-query
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTanStack Query (React Query) v5
TanStack Query (React Query) v5
Last Updated: 2026-01-20
Versions: @tanstack/react-query@5.90.19, @tanstack/react-query-devtools@5.91.2
Requires: React 18.0+ (useSyncExternalStore), TypeScript 4.7+ (recommended)
最后更新:2026-01-20
版本:@tanstack/react-query@5.90.19, @tanstack/react-query-devtools@5.91.2
依赖要求:React 18.0+(useSyncExternalStore),TypeScript 4.7+(推荐)
v5 New Features
v5 新特性
useMutationState - Cross-Component Mutation Tracking
useMutationState - 跨组件变更追踪
Access mutation state from anywhere without prop drilling:
tsx
import { useMutationState } from '@tanstack/react-query'
function GlobalLoadingIndicator() {
// Get all pending mutations
const pendingMutations = useMutationState({
filters: { status: 'pending' },
select: (mutation) => mutation.state.variables,
})
if (pendingMutations.length === 0) return null
return <div>Saving {pendingMutations.length} items...</div>
}
// Filter by mutation key
const todoMutations = useMutationState({
filters: { mutationKey: ['addTodo'] },
})无需属性透传,即可从任意位置访问变更状态:
tsx
import { useMutationState } from '@tanstack/react-query'
function GlobalLoadingIndicator() {
// 获取所有待处理的变更
const pendingMutations = useMutationState({
filters: { status: 'pending' },
select: (mutation) => mutation.state.variables,
})
if (pendingMutations.length === 0) return null
return <div>正在保存 {pendingMutations.length} 个条目...</div>
}
// 按变更键筛选
const todoMutations = useMutationState({
filters: { mutationKey: ['addTodo'] },
})Simplified Optimistic Updates
简化的乐观更新
New pattern using - no cache manipulation, no rollback needed:
variablestsx
function TodoList() {
const { data: todos } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
const addTodo = useMutation({
mutationKey: ['addTodo'],
mutationFn: (newTodo) => api.addTodo(newTodo),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
// Show optimistic UI using variables from pending mutations
const pendingTodos = useMutationState({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => mutation.state.variables,
})
return (
<ul>
{todos?.map(todo => <li key={todo.id}>{todo.title}</li>)}
{/* Show pending items with visual indicator */}
{pendingTodos.map((todo, i) => (
<li key={`pending-${i}`} style={{ opacity: 0.5 }}>{todo.title}</li>
))}
</ul>
)
}使用的新模式 - 无需缓存操作,无需回滚:
variablestsx
function TodoList() {
const { data: todos } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
const addTodo = useMutation({
mutationKey: ['addTodo'],
mutationFn: (newTodo) => api.addTodo(newTodo),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
// 使用待处理变更中的variables显示乐观UI
const pendingTodos = useMutationState({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => mutation.state.variables,
})
return (
<ul>
{todos?.map(todo => <li key={todo.id}>{todo.title}</li>)}
{/* 显示带视觉标识的待处理条目 */}
{pendingTodos.map((todo, i) => (
<li key={`pending-${i}`} style={{ opacity: 0.5 }}>{todo.title}</li>
))}
</ul>
)
}throwOnError - Error Boundaries
throwOnError - 错误边界
Renamed from (breaking change):
useErrorBoundarytsx
import { QueryErrorResetBoundary } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'
function App() {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => (
<div>
Error! <button onClick={resetErrorBoundary}>Retry</button>
</div>
)}>
<Todos />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
)
}
function Todos() {
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
throwOnError: true, // ✅ v5 (was useErrorBoundary in v4)
})
return <div>{data.map(...)}</div>
}从重命名而来(破坏性变更):
useErrorBoundarytsx
import { QueryErrorResetBoundary } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'
function App() {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => (
<div>
出错了!<button onClick={resetErrorBoundary}>重试</button>
</div>
)}>
<Todos />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
)
}
function Todos() {
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
throwOnError: true, // ✅ v5(v4中为useErrorBoundary)
})
return <div>{data.map(...)}</div>
}Network Mode (Offline/PWA Support)
网络模式(离线/PWA支持)
Control behavior when offline:
tsx
const queryClient = new QueryClient({
defaultOptions: {
queries: {
networkMode: 'offlineFirst', // Use cache when offline
},
},
})
// Per-query override
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
networkMode: 'always', // Always try, even offline (for local APIs)
})| Mode | Behavior |
|---|---|
| Only fetch when online |
| Always try (useful for local/service worker APIs) |
| Use cache first, fetch when online |
Detecting paused state:
tsx
const { isPending, fetchStatus } = useQuery(...)
// isPending + fetchStatus === 'paused' = offline, waiting for network离线时控制行为:
tsx
const queryClient = new QueryClient({
defaultOptions: {
queries: {
networkMode: 'offlineFirst', // 离线时使用缓存
},
},
})
// 按查询覆盖
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
networkMode: 'always', // 始终尝试,即使离线(适用于本地API)
})| 模式 | 行为 |
|---|---|
| 仅在线时获取数据 |
| 始终尝试(适用于本地/Service Worker API) |
| 优先使用缓存,在线时获取最新数据 |
检测暂停状态:
tsx
const { isPending, fetchStatus } = useQuery(...)
// isPending + fetchStatus === 'paused' = 离线,等待网络恢复useQueries with Combine
useQueries 结合结果
Combine results from parallel queries:
tsx
const results = useQueries({
queries: userIds.map(id => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
})),
combine: (results) => ({
data: results.map(r => r.data),
pending: results.some(r => r.isPending),
error: results.find(r => r.error)?.error,
}),
})
// Access combined result
if (results.pending) return <Loading />
console.log(results.data) // [user1, user2, user3]合并并行查询的结果:
tsx
const results = useQueries({
queries: userIds.map(id => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
})),
combine: (results) => ({
data: results.map(r => r.data),
pending: results.some(r => r.isPending),
error: results.find(r => r.error)?.error,
}),
})
// 访问合并后的结果
if (results.pending) return <Loading />
console.log(results.data) // [user1, user2, user3]infiniteQueryOptions Helper
infiniteQueryOptions 辅助函数
Type-safe factory for infinite queries (parallel to ):
queryOptionstsx
import { infiniteQueryOptions, useInfiniteQuery, prefetchInfiniteQuery } from '@tanstack/react-query'
const todosInfiniteOptions = infiniteQueryOptions({
queryKey: ['todos', 'infinite'],
queryFn: ({ pageParam }) => fetchTodosPage(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
// Reuse across hooks
useInfiniteQuery(todosInfiniteOptions)
useSuspenseInfiniteQuery(todosInfiniteOptions)
prefetchInfiniteQuery(queryClient, todosInfiniteOptions)用于无限查询的类型安全工厂(与并行):
queryOptionstsx
import { infiniteQueryOptions, useInfiniteQuery, prefetchInfiniteQuery } from '@tanstack/react-query'
const todosInfiniteOptions = infiniteQueryOptions({
queryKey: ['todos', 'infinite'],
queryFn: ({ pageParam }) => fetchTodosPage(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
// 在钩子中复用
useInfiniteQuery(todosInfiniteOptions)
useSuspenseInfiniteQuery(todosInfiniteOptions)
prefetchInfiniteQuery(queryClient, todosInfiniteOptions)maxPages - Memory Optimization
maxPages - 内存优化
Limit pages stored in cache for infinite queries:
tsx
useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam }) => fetchPosts(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
getPreviousPageParam: (firstPage) => firstPage.prevCursor, // Required with maxPages
maxPages: 3, // Only keep 3 pages in memory
})Note: requires bi-directional pagination ( AND ).
maxPagesgetNextPageParamgetPreviousPageParam限制无限查询在缓存中存储的页面数量:
tsx
useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam }) => fetchPosts(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
getPreviousPageParam: (firstPage) => firstPage.prevCursor, // 使用maxPages时必填
maxPages: 3, // 仅在内存中保留3个页面
})注意: 需要双向分页(同时配置和)。
maxPagesgetNextPageParamgetPreviousPageParamQuick Setup
快速设置
bash
npm install @tanstack/react-query@latest
npm install -D @tanstack/react-query-devtools@latestbash
npm install @tanstack/react-query@latest
npm install -D @tanstack/react-query-devtools@latestStep 2: Provider + Config
步骤2:Provider + 配置
tsx
// src/main.tsx
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 (v5: renamed from cacheTime)
refetchOnWindowFocus: false,
},
},
})
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>tsx
// src/main.tsx
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小时(v5:从cacheTime重命名)
refetchOnWindowFocus: false,
},
},
})
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>Step 3: Query + Mutation Hooks
步骤3:查询与变更钩子
tsx
// src/hooks/useTodos.ts
import { useQuery, useMutation, useQueryClient, queryOptions } from '@tanstack/react-query'
// Query options factory (v5 pattern)
export 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()
},
})
export function useTodos() {
return useQuery(todosQueryOptions)
}
export function useAddTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (newTodo) => {
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'] })
},
})
}
// Usage:
function TodoList() {
const { data, isPending, isError, error } = useTodos()
const { mutate } = useAddTodo()
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>
}tsx
// src/hooks/useTodos.ts
import { useQuery, useMutation, useQueryClient, queryOptions } from '@tanstack/react-query'
// 查询选项工厂(v5模式)
export const todosQueryOptions = queryOptions({
queryKey: ['todos'],
queryFn: async () => {
const res = await fetch('/api/todos')
if (!res.ok) throw new Error('获取失败')
return res.json()
},
})
export function useTodos() {
return useQuery(todosQueryOptions)
}
export function useAddTodo() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (newTodo) => {
const res = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTodo),
})
if (!res.ok) throw new Error('添加失败')
return res.json()
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
}
// 使用示例:
function TodoList() {
const { data, isPending, isError, error } = useTodos()
const { mutate } = useAddTodo()
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>
}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 <Loading />
// 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} />}❌ Never rely on refetchOnMount: false for errored queries
tsx
// Doesn't work - errors are always stale
useQuery({
queryKey: ['data'],
queryFn: failingFetch,
refetchOnMount: false, // ❌ Ignored when query has error
})
// Use retryOnMount instead
useQuery({
queryKey: ['data'],
queryFn: failingFetch,
refetchOnMount: false,
retryOnMount: false, // ✅ Prevents refetch for errored queries
retry: 0,
})❌ 禁止使用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 // ❌ 现在表示“待处理且正在获取”
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} />}❌ 不要依赖refetchOnMount: false处理出错的查询
tsx
// 无效 - 出错的查询始终视为过期
useQuery({
queryKey: ['data'],
queryFn: failingFetch,
refetchOnMount: false, // ❌ 查询出错时会被忽略
})
// 改用retryOnMount
useQuery({
queryKey: ['data'],
queryFn: failingFetch,
refetchOnMount: false,
retryOnMount: false, // ✅ 防止出错的查询在挂载时重新获取
retry: 0,
})Known Issues Prevention
已知问题预防
This skill prevents 16 documented issues from v5 migration, SSR/hydration bugs, and common mistakes:
本内容可预防v5迁移、SSR/水合错误以及常见错误中的16个已记录问题:
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({ queryKey, queryFn, ...options })
useQuery is not a functionv4写法:
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已移除(变更中仍可使用)
预防:使用处理副作用,或将逻辑移至变更回调
useEffectv4写法:
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重命名
原因:重命名为,含义变更
预防:使用表示初始加载,表示“待办且正在获取”
status: 'loading'status: 'pending'isLoadingisPendingisLoadingv4写法:
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保证数据可用,无法条件禁用
预防:使用条件渲染替代选项
enabledv4/错误写法:
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 requiredundefinedinitialPageParamv4写法:
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: keepPreviousDatav4写法:
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写法(错误为unknown):
tsx
const { error } = useQuery({
queryKey: ['data'],
queryFn: async () => {
if (Math.random() > 0.5) throw '自定义错误字符串'
return data
},
})
// error: unknownv5写法(指定自定义错误类型):
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(默认)Issue #9: Streaming Server Components Hydration Error
问题9:流式服务端组件水合错误
Error:
Source: GitHub Issue #9642
Affects: v5.82.0+ with streaming SSR (void prefetch pattern)
Why It Happens: Race condition where resolves synchronously but creates async retryer, causing isFetching/isStale mismatch between server and client
Prevention: Don't conditionally render based on with and streaming prefetch, OR await prefetch instead of void pattern
Hydration failed because the initial UI does not match what was rendered on the serverhydrate()query.fetch()fetchStatususeSuspenseQueryBefore (causes hydration error):
tsx
// Server: void prefetch
streamingQueryClient.prefetchQuery({ queryKey: ['data'], queryFn: getData });
// Client: conditional render on fetchStatus
const { data, isFetching } = useSuspenseQuery({ queryKey: ['data'], queryFn: getData });
return <>{data && <div>{data}</div>} {isFetching && <Loading />}</>;After (workaround):
tsx
// Option 1: Await prefetch
await streamingQueryClient.prefetchQuery({ queryKey: ['data'], queryFn: getData });
// Option 2: Don't render based on fetchStatus with Suspense
const { data } = useSuspenseQuery({ queryKey: ['data'], queryFn: getData });
return <div>{data}</div>; // No conditional on isFetchingStatus: Known issue, being investigated by maintainers. Requires implementation of in useSyncExternalStore.
getServerSnapshot错误:
来源:GitHub问题#9642
影响版本:v5.82.0+ 搭配流式SSR(void预取模式)
原因:竞态条件下,同步解析但创建异步重试器,导致服务端与客户端的isFetching/isStale不匹配
预防:使用和流式预取时,不要基于条件渲染;或使用await预取替代void模式
Hydration failed because the initial UI does not match what was rendered on the serverhydrate()query.fetch()useSuspenseQueryfetchStatus错误写法(导致水合错误):
tsx
// 服务端:void预取
streamingQueryClient.prefetchQuery({ queryKey: ['data'], queryFn: getData });
// 客户端:基于fetchStatus条件渲染
const { data, isFetching } = useSuspenseQuery({ queryKey: ['data'], queryFn: getData });
return <>{data && <div>{data}</div>} {isFetching && <Loading />}</>;修复写法:
tsx
// 选项1:Await预取
await streamingQueryClient.prefetchQuery({ queryKey: ['data'], queryFn: getData });
// 选项2:使用Suspense时不要基于fetchStatus渲染
const { data } = useSuspenseQuery({ queryKey: ['data'], queryFn: getData });
return <div>{data}</div>; // 不要基于isFetching条件渲染状态:已知问题,维护者正在调查。需要在useSyncExternalStore中实现。
getServerSnapshotIssue #10: useQuery Hydration Error with Prefetching
问题10:预取时useQuery水合错误
Error: Text content mismatch during hydration
Source: GitHub Issue #9399
Affects: v5.x with server-side prefetching
Why It Happens: detects resolved promises in RSC payload and extracts data synchronously during hydration, bypassing normal pending state
Prevention: Use instead of for SSR, or avoid conditional rendering based on
tryResolveSyncuseSuspenseQueryuseQueryisLoadingBefore (causes hydration error):
tsx
// Server Component
const queryClient = getServerQueryClient();
await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos });
// Client Component
function Todos() {
const { data, isLoading } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
if (isLoading) return <div>Loading...</div>; // Server renders this
return <div>{data.length} todos</div>; // Client hydrates with this
}After (workaround):
tsx
// Use useSuspenseQuery instead
function Todos() {
const { data } = useSuspenseQuery({ queryKey: ['todos'], queryFn: fetchTodos });
return <div>{data.length} todos</div>;
}Status: "At the top of my OSS list of things to fix" - maintainer Ephem (Nov 2025). Requires implementing in useSyncExternalStore.
getServerSnapshot错误:水合期间文本内容不匹配
来源:GitHub问题#9399
影响版本:v5.x 搭配服务端预取
原因:在RSC payload中检测到已解析的promise,并在水合期间同步提取数据,绕过了正常的待处理状态
预防:SSR时使用替代,或避免基于条件渲染
tryResolveSyncuseSuspenseQueryuseQueryisLoading错误写法(导致水合错误):
tsx
// 服务端组件
const queryClient = getServerQueryClient();
await queryClient.prefetchQuery({ queryKey: ['todos'], queryFn: fetchTodos });
// 客户端组件
function Todos() {
const { data, isLoading } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
if (isLoading) return <div>加载中...</div>; // 服务端渲染此内容
return <div>{data.length}个待办事项</div>; // 客户端水合此内容
}修复写法:
tsx
// 使用useSuspenseQuery替代
function Todos() {
const { data } = useSuspenseQuery({ queryKey: ['todos'], queryFn: fetchTodos });
return <div>{data.length}个待办事项</div>;
}状态:"在我的OSS修复列表顶部" - 维护者Ephem(2025年11月)。需要在useSyncExternalStore中实现。
getServerSnapshotIssue #11: refetchOnMount Not Respected for Errored Queries
问题11:refetchOnMount对出错的查询不生效
Error: Queries refetch on mount despite
Source: GitHub Issue #10018
Affects: v5.90.16+
Why It Happens: Errored queries with no data are always treated as stale. This is intentional to avoid permanently showing error states
Prevention: Use instead of (or in addition to)
refetchOnMount: falseretryOnMount: falserefetchOnMount: falseBefore (refetches despite setting):
tsx
const { data, error } = useQuery({
queryKey: ['data'],
queryFn: () => { throw new Error('Fails') },
refetchOnMount: false, // Ignored when query is in error state
retry: 0,
});
// Query refetches every time component mountsAfter (correct):
tsx
const { data, error } = useQuery({
queryKey: ['data'],
queryFn: failingFetch,
refetchOnMount: false,
retryOnMount: false, // ✅ Prevents refetch on mount for errored queries
retry: 0,
});Status: Documented behavior (intentional). The name is slightly misleading - it controls whether errored queries trigger a new fetch on mount, not automatic retries.
retryOnMount错误:尽管设置了,查询仍在挂载时重新获取
来源:GitHub问题#10018
影响版本:v5.90.16+
原因:无数据的出错查询始终被视为过期。这是为了避免永久显示错误状态的有意设计
预防:使用替代(或补充)
refetchOnMount: falseretryOnMount: falserefetchOnMount: false错误写法(仍会重新获取):
tsx
const { data, error } = useQuery({
queryKey: ['data'],
queryFn: () => { throw new Error('失败') },
refetchOnMount: false, // 查询出错时会被忽略
retry: 0,
});
// 每次组件挂载时查询都会重新获取正确写法:
tsx
const { data, error } = useQuery({
queryKey: ['data'],
queryFn: failingFetch,
refetchOnMount: false,
retryOnMount: false, // ✅ 防止出错的查询在挂载时重新获取
retry: 0,
});状态:已记录的有意行为。名称略有误导 - 它控制出错的查询是否在挂载时触发新的获取,而非自动重试。
retryOnMountIssue #12: Mutation Callback Signature Breaking Change (v5.89.0)
问题12:变更回调签名破坏性变更(v5.89.0)
Error: TypeScript errors in mutation callbacks
Source: GitHub Issue #9660
Affects: v5.89.0+
Why It Happens: parameter added between and , changing callback signatures from 3 params to 4
Prevention: Update all mutation callbacks to accept 4 parameters instead of 3
onMutateResultvariablescontextBefore (v5.88 and earlier):
tsx
useMutation({
mutationFn: addTodo,
onError: (error, variables, context) => {
// context is now onMutateResult, missing final context param
},
onSuccess: (data, variables, context) => {
// Same issue
}
});After (v5.89.0+):
tsx
useMutation({
mutationFn: addTodo,
onError: (error, variables, onMutateResult, context) => {
// onMutateResult = return value from onMutate
// context = mutation function context
},
onSuccess: (data, variables, onMutateResult, context) => {
// Correct signature with 4 parameters
}
});Note: If you don't use , the parameter will be undefined. This breaking change was introduced in a patch version.
onMutateonMutateResult错误:变更回调中的TypeScript错误
来源:GitHub问题#9660
影响版本:v5.89.0+
原因:在和之间添加了参数,将回调签名从3个参数改为4个
预防:将所有变更回调更新为接受4个参数而非3个
variablescontextonMutateResultv5.88及更早版本写法:
tsx
useMutation({
mutationFn: addTodo,
onError: (error, variables, context) => {
// context现在是onMutateResult,缺少最终的context参数
},
onSuccess: (data, variables, context) => {
// 同样问题
}
});v5.89.0+写法:
tsx
useMutation({
mutationFn: addTodo,
onError: (error, variables, onMutateResult, context) => {
// onMutateResult = onMutate的返回值
// context = 变更函数上下文
},
onSuccess: (data, variables, onMutateResult, context) => {
// 正确的4参数签名
}
});注意:如果不使用,参数将为undefined。此破坏性变更是在补丁版本中引入的。
onMutateonMutateResultIssue #13: Readonly Query Keys Break Partial Matching (v5.90.8)
问题13:只读查询键破坏部分匹配(v5.90.8)
Error:
Source: GitHub Issue #9871 | Fixed in PR #9872
Affects: v5.90.8 only (fixed in v5.90.9)
Why It Happens: Partial query matching broke TypeScript types for readonly query keys (using )
Prevention: Upgrade to v5.90.9+ or use type assertions if stuck on v5.90.8
Type 'readonly ["todos", string]' is not assignable to type '["todos", string]'as constBefore (v5.90.8 - TypeScript error):
tsx
export function todoQueryKey(id?: string) {
return id ? ['todos', id] as const : ['todos'] as const;
}
// Type: readonly ['todos', string] | readonly ['todos']
useMutation({
mutationFn: addTodo,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: todoQueryKey('123')
// Error: readonly ['todos', string] not assignable to ['todos', string]
});
}
});After (v5.90.9+):
tsx
// Works correctly with readonly types
queryClient.invalidateQueries({
queryKey: todoQueryKey('123') // ✅ No type error
});Status: Fixed in v5.90.9. Particularly affected users of code generators like that produce readonly query keys.
openapi-react-query错误:
来源:GitHub问题#9871 | 已在PR #9872中修复
影响版本:仅v5.90.8(v5.90.9中已修复)
原因:部分查询匹配破坏了只读查询键的TypeScript类型(使用)
预防:升级到v5.90.9+,或如果停留在v5.90.8则使用类型断言
Type 'readonly ["todos", string]' is not assignable to type '["todos", string]'as constv5.90.8写法(TypeScript错误):
tsx
export function todoQueryKey(id?: string) {
return id ? ['todos', id] as const : ['todos'] as const;
}
// 类型:readonly ['todos', string] | readonly ['todos']
useMutation({
mutationFn: addTodo,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: todoQueryKey('123')
// 错误:readonly ['todos', string] 无法分配给 ['todos', string]
});
}
});v5.90.9+写法:
tsx
// 对只读类型正常工作
queryClient.invalidateQueries({
queryKey: todoQueryKey('123') // ✅ 无类型错误
});状态:已在v5.90.9中修复。尤其影响使用代码生成器(如)生成只读查询键的用户。
openapi-react-queryIssue #14: useMutationState Type Inference Lost
问题14:useMutationState类型推断丢失
Error: typed as instead of actual type
Source: GitHub Issue #9825
Affects: All v5.x versions
Why It Happens: Fuzzy mutation key matching prevents guaranteed type inference (same issue as )
Prevention: Explicitly cast types in the callback
mutation.state.variablesunknownqueryClient.getQueryCache().find()selectBefore (type inference doesn't work):
tsx
const addTodo = useMutation({
mutationKey: ['addTodo'],
mutationFn: (todo: Todo) => api.addTodo(todo),
});
const pendingTodos = useMutationState({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => {
return mutation.state.variables; // Type: unknown
},
});After (with explicit cast):
tsx
const pendingTodos = useMutationState({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => mutation.state.variables as Todo,
});
// Or cast the entire state:
select: (mutation) => mutation.state as MutationState<Todo, Error, Todo, unknown>Status: Known limitation of fuzzy matching. No planned fix.
错误:被推断为而非实际类型
来源:GitHub问题#9825
影响版本:所有v5.x版本
原因:模糊的查询键匹配导致无法保证类型推断(与问题相同)
预防:在回调中显式转换类型
mutation.state.variablesunknownqueryClient.getQueryCache().find()select错误写法(类型推断不生效):
tsx
const addTodo = useMutation({
mutationKey: ['addTodo'],
mutationFn: (todo: Todo) => api.addTodo(todo),
});
const pendingTodos = useMutationState({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => {
return mutation.state.variables; // 类型:unknown
},
});正确写法(显式转换):
tsx
const pendingTodos = useMutationState({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => mutation.state.variables as Todo,
});
// 或转换整个状态:
select: (mutation) => mutation.state as MutationState<Todo, Error, Todo, unknown>状态:模糊匹配的已知限制。暂无修复计划。
Issue #15: Query Cancellation in StrictMode with fetchQuery
问题15:StrictMode中fetchQuery的查询取消
Error: when using with
Source: GitHub Issue #9798
Affects: Development only (React StrictMode)
Why It Happens: StrictMode causes double mount/unmount. When unmounts and is the last observer, it cancels the query even if is also running
Prevention: This is expected development-only behavior. Doesn't affect production
CancelledErrorfetchQuery()useQueryuseQueryfetchQuery()Example:
tsx
async function loadData() {
try {
const data = await queryClient.fetchQuery({
queryKey: ['data'],
queryFn: fetchData,
});
console.log('Loaded:', data); // Never logs in StrictMode
} catch (error) {
console.error('Failed:', error); // CancelledError
}
}
function Component() {
const { data } = useQuery({ queryKey: ['data'], queryFn: fetchData });
// In StrictMode, component unmounts/remounts, cancelling fetchQuery
}Workaround:
tsx
// Keep query observed with staleTime
const { data } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
staleTime: Infinity, // Keeps query active
});Status: Expected StrictMode behavior, not a bug. Production builds are unaffected.
错误:使用和时出现
来源:GitHub问题#9798
影响版本:仅开发环境(React StrictMode)
原因:StrictMode导致双重挂载/卸载。当卸载且是最后一个观察者时,即使也在运行,它会取消查询
预防:这是预期的开发环境行为。不影响生产环境
fetchQuery()useQueryCancelledErroruseQueryfetchQuery()示例:
tsx
async function loadData() {
try {
const data = await queryClient.fetchQuery({
queryKey: ['data'],
queryFn: fetchData,
});
console.log('已加载:', data); // StrictMode中永远不会打印
} catch (error) {
console.error('失败:', error); // CancelledError
}
}
function Component() {
const { data } = useQuery({ queryKey: ['data'], queryFn: fetchData });
// StrictMode中,组件卸载/重新挂载,取消fetchQuery
}解决方法:
tsx
// 使用staleTime保持查询被观察
const { data } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
staleTime: Infinity, // 保持查询活跃
});状态:StrictMode的预期行为,不是bug。生产构建不受影响。
Issue #16: invalidateQueries Only Refetches Active Queries
问题16:invalidateQueries仅重新获取活跃查询
Error: Inactive queries not refetching despite call
Source: GitHub Issue #9531
Affects: All v5.x versions
Why It Happens: Documentation was misleading - only refetches "active" queries by default, not "all" queries
Prevention: Use to force refetch of inactive queries
invalidateQueries()invalidateQueries()refetchType: 'all'Default behavior:
tsx
// Only active queries (currently being observed) will refetch
queryClient.invalidateQueries({ queryKey: ['todos'] });To refetch inactive queries:
tsx
queryClient.invalidateQueries({
queryKey: ['todos'],
refetchType: 'all' // Refetch active AND inactive
});Status: Documentation fixed to clarify "active" queries. This is the intended behavior.
错误:尽管调用了,非活跃查询仍未重新获取
来源:GitHub问题#9531
影响版本:所有v5.x版本
原因:文档存在误导 - 默认情况下仅重新获取“活跃”查询,而非“所有”查询
预防:使用强制重新获取非活跃查询
invalidateQueries()invalidateQueries()refetchType: 'all'默认行为:
tsx
// 仅活跃查询(当前被观察)会重新获取
queryClient.invalidateQueries({ queryKey: ['todos'] });重新获取非活跃查询:
tsx
queryClient.invalidateQueries({
queryKey: ['todos'],
refetchType: 'all' // 重新获取活跃和非活跃查询
});状态:文档已修复以明确“活跃”查询。这是预期行为。
Community Tips
社区技巧
Note: These tips come from community experts and maintainer blogs. Verify against your version.
注意:这些技巧来自社区专家和维护者博客。请根据你的版本验证。
Tip: Query Options with Multiple Listeners
技巧:多监听器的查询选项
Source: TkDodo's Blog - API Design Lessons | Confidence: HIGH
Applies to: v5.27.3+
When multiple components use the same query with different options (like ), the "last write wins" rule applies for future fetches, but the current in-flight query uses its original options. This can cause unexpected behavior when components mount at different times.
staleTimeExample of unexpected behavior:
tsx
// Component A mounts first
function ComponentA() {
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5000, // Applied initially
});
}
// Component B mounts while A's query is in-flight
function ComponentB() {
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 60000, // Won't affect current fetch, only future ones
});
}Recommended approach:
tsx
// Write options as functions that reference latest values
const getStaleTime = () => shouldUseLongCache ? 60000 : 5000;
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: getStaleTime(), // Evaluated on each render
});来源:TkDodo的博客 - API设计经验 | 可信度:高
适用版本:v5.27.3+
当多个组件使用同一查询但选项不同(如)时,“最后写入获胜”规则适用于未来的获取,但当前进行中的查询使用其原始选项。当组件在不同时间挂载时,这可能导致意外行为。
staleTime意外行为示例:
tsx
// 组件A先挂载
function ComponentA() {
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5000, // 初始应用
});
}
// 组件B在A的查询进行中挂载
function ComponentB() {
const { data } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 60000, // 不会影响当前获取,仅影响未来获取
});
}推荐方式:
tsx
// 将选项编写为引用最新值的函数
const getStaleTime = () => shouldUseLongCache ? 60000 : 5000;
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: getStaleTime(), // 每次渲染时评估
});Tip: refetch() is NOT for Changed Parameters
技巧:refetch()不应用于参数变更
Source: Avoiding Common Mistakes with TanStack Query | Confidence: HIGH
The function should ONLY be used for refreshing with the same parameters (like a manual "reload" button). For new parameters (filters, page numbers, search terms, etc.), include them in the query key instead.
refetch()Anti-pattern:
tsx
// ❌ Wrong - using refetch() for different parameters
const [page, setPage] = useState(1);
const { data, refetch } = useQuery({
queryKey: ['todos'], // Same key for all pages
queryFn: () => fetchTodos(page),
});
// This refetches with OLD page value, not new one
<button onClick={() => { setPage(2); refetch(); }}>Next</button>Correct pattern:
tsx
// ✅ Correct - include parameters in query key
const [page, setPage] = useState(1);
const { data } = useQuery({
queryKey: ['todos', page], // Key changes with page
queryFn: () => fetchTodos(page),
// Query automatically refetches when page changes
});
<button onClick={() => setPage(2)}>Next</button> // Just update stateWhen to use refetch():
tsx
// ✅ Manual refresh of same data (refresh button)
const { data, refetch } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
});
<button onClick={() => refetch()}>Refresh</button> // Same parameters来源:避免TanStack Query的常见错误 | 可信度:高
refetch()反模式:
tsx
// ❌ 错误 - 使用refetch()处理不同参数
const [page, setPage] = useState(1);
const { data, refetch } = useQuery({
queryKey: ['todos'], // 所有页面使用相同键
queryFn: () => fetchTodos(page),
});
// 这会使用旧页面值重新获取,而非新值
<button onClick={() => { setPage(2); refetch(); }}>下一页</button>正确模式:
tsx
// ✅ 正确 - 将参数包含在查询键中
const [page, setPage] = useState(1);
const { data } = useQuery({
queryKey: ['todos', page], // 键随页面变更
queryFn: () => fetchTodos(page),
// 页面变更时查询自动重新获取
});
<button onClick={() => setPage(2)}>下一页</button> // 只需更新状态何时使用refetch():
tsx
// ✅ 手动刷新相同数据(刷新按钮)
const { data, refetch } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
});
<button onClick={() => refetch()}>刷新</button> // 相同参数Key Patterns
关键模式
Dependent Queries (Query B waits for Query A):
tsx
const { data: posts } = useQuery({
queryKey: ['users', userId, 'posts'],
queryFn: () => fetchUserPosts(userId),
enabled: !!user, // Wait for user
})Parallel Queries (fetch multiple at once):
tsx
const results = useQueries({
queries: ids.map(id => ({ queryKey: ['todos', id], queryFn: () => fetchTodo(id) })),
})Prefetching (preload on hover):
tsx
queryClient.prefetchQuery({ queryKey: ['todo', id], queryFn: () => fetchTodo(id) })Infinite Scroll (useInfiniteQuery):
tsx
useInfiniteQuery({
queryKey: ['todos', 'infinite'],
queryFn: ({ pageParam }) => fetchTodosPage(pageParam),
initialPageParam: 0, // Required in v5
getNextPageParam: (lastPage) => lastPage.nextCursor,
})Query Cancellation (auto-cancel on queryKey change):
tsx
queryFn: async ({ signal }) => {
const res = await fetch(`/api/todos?q=${search}`, { signal })
return res.json()
}Data Transformation (select):
tsx
select: (data) => data.filter(todo => todo.completed)Avoid Request Waterfalls: Fetch in parallel when possible (don't chain queries unless truly dependent)
Official Docs: https://tanstack.com/query/latest | v5 Migration: https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5 | GitHub: https://github.com/TanStack/query | Context7:
/websites/tanstack_query依赖查询(查询B等待查询A):
tsx
const { data: posts } = useQuery({
queryKey: ['users', userId, 'posts'],
queryFn: () => fetchUserPosts(userId),
enabled: !!user, // 等待用户数据
})并行查询(同时获取多个):
tsx
const results = useQueries({
queries: ids.map(id => ({ queryKey: ['todos', id], queryFn: () => fetchTodo(id) })),
})预取(悬停时预加载):
tsx
queryClient.prefetchQuery({ queryKey: ['todo', id], queryFn: () => fetchTodo(id) })无限滚动(useInfiniteQuery):
tsx
useInfiniteQuery({
queryKey: ['todos', 'infinite'],
queryFn: ({ pageParam }) => fetchTodosPage(pageParam),
initialPageParam: 0, // v5中必填
getNextPageParam: (lastPage) => lastPage.nextCursor,
})查询取消(查询键变更时自动取消):
tsx
queryFn: async ({ signal }) => {
const res = await fetch(`/api/todos?q=${search}`, { signal })
return res.json()
}数据转换(select):
tsx
select: (data) => data.filter(todo => todo.completed)避免请求瀑布流:尽可能并行获取(除非真正依赖,否则不要链式查询)
官方文档:https://tanstack.com/query/latest | v5迁移指南:https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5 | GitHub:https://github.com/TanStack/query | Context7:",
/websites/tanstack_query