关于 tanstack-query
面向 React 应用的 TanStack Query v5 技能指南,涵盖查询与变更模式、版本迁移要点及常见问题的规避方法。
主要功能
- 01查询与变更 Hook 模式
- 02乐观更新与变更状态跟踪
- 03支持离线的网络模式
- 04并行查询与无限查询指南
- 05v5 迁移问题及处理方式
适用场景
- 01构建 React 数据获取流程
- 02跨组件跟踪待处理变更
- 03离线时使用缓存查询
- 04将应用从 v4 迁移至 v5
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill tanstack-query安装后在 Claude Code 中输入/tanstack-query确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
README.md.claude-plugin/plugin.jsonassets/example-template.txtreferences/best-practices.mdcommon-patterns.mdexample-reference.mdtesting.mdtop-errors.mdtypescript-patterns.mdv4-to-v5-migration.mdrules/tanstack-query.mdscripts/example-script.shtemplates/custom-hooks-pattern.tsxdevtools-setup.tsxerror-boundary.tsxpackage.jsonprovider-setup.tsxquery-client-config.tsuse-infinite-query.tsxuse-mutation-basic.tsxuse-mutation-optimistic.tsxuse-query-basic.tsx详细介绍
TanStack Query(React Query)v5
最后更新:2026-01-20 版本:@tanstack/[email protected]、@tanstack/[email protected] 要求:React 18.0+(useSyncExternalStore)、TypeScript 4.7+(推荐)
v5 新功能
useMutationState - 跨组件变更跟踪
无需逐层传递 props,即可从任何位置访问变更状态:
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'] },
})
简化乐观更新
使用 variables 的新模式——无需操作缓存,也无需回滚:
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>
)
}
throwOnError - 错误边界
从 useErrorBoundary 重命名而来(存在破坏性变更):
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>
}
网络模式(离线/PWA 支持)
控制离线时的行为:
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)
})
| 模式 | 行为 |
|---|---|
online(默认) | 仅在联网时获取 |
always | 始终尝试(适用于本地 API/Service Worker API) |
offlineFirst | 优先使用缓存,联网后再获取 |
检测暂停状态:
const { isPending, fetchStatus } = useQuery(...)
// isPending + fetchStatus === 'paused' = offline, waiting for network
带 Combine 的 useQueries
合并并行查询的结果:
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]
infiniteQueryOptions 辅助函数
用于无限查询的类型安全工厂(与 queryOptions 相对应):
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)
maxPages - 内存优化
限制无限查询在缓存中存储的页面数:
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
})
注意:maxPages 要求双向分页(getNextPageParam 和 getPreviousPageParam)。
快速设置
npm install @tanstack/react-query@latest
npm install -D @tanstack/react-query-devtools@latest
第 2 步:Provider + 配置
// 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>
第 3 步:查询和变更 Hook
// 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>
}
关键规则
始终遵循
✅ 所有 Hook 均使用对象语法
// v5 ONLY supports this:
useQuery({ queryKey, queryFn, ...options })
useMutation({ mutationFn, ...options })
✅ 查询键使用数组
queryKey: ['todos'] // List
queryKey: ['todos', id] // Detail
queryKey: ['todos', { filter }] // Filtered
✅ 合理配置 staleTime
staleTime: 1000 * 60 * 5 // 5 min - prevents excessive refetches
✅ 初始加载状态使用 isPending
if (isPending) return <Loading />
// isPending = no data yet AND fetching
✅ 在 queryFn 中抛出错误
if (!response.ok) throw new Error('Failed')
✅ 变更后使查询失效
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
}
✅ 使用 queryOptions 工厂实现可复用模式
const opts = queryOptions({ queryKey, queryFn })
useQuery(opts)
useSuspenseQuery(opts)
prefetchQuery(opts)
✅ 使用 gcTime(而非 cacheTime)
gcTime: 1000 * 60 * 60 // 1 hour
绝不遵循
❌ 绝不使用 v4 数组/函数语法
// v4 (removed in v5):
useQuery(['todos'], fetchTodos, options) // ❌
// v5 (correct):
useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) // ✅
❌ 绝不使用查询回调(查询中的 onSuccess、onError、onSettled)
// 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
})
❌ 绝不使用已弃用的选项
// 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
❌ 绝不假设 isLoading 表示“尚无数据”
// v5 changed this:
isLoading = isPending && isFetching // ❌ Now means "pending AND fetching"
isPending = no data yet // ✅ Use this for initial load
❌ 无限查询中绝不遗漏 initialPageParam
// v5 requires this:
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam }) => fetchProjects(pageParam),
initialPageParam: 0, // ✅ Required in v5
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
❌ 绝不将 enabled 与 useSuspenseQuery 搭配使用
// Not allowed:
useSuspenseQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
enabled: !!id, // ❌ Not available with suspense
})
// Use conditional rendering instead:
{id && <TodoComponent id={id} />}
❌ 绝不依赖 refetchOnMount: false 处理出错的查询
// 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,
})
已知问题预防
此技能可预防 v5 迁移、SSR/水合错误和常见错误中记录的 16 个问题:
问题 #1:必须使用对象语法
错误:useQuery is not a function 或类型错误
来源:v5 迁移指南
发生原因:v5 移除了所有函数重载,仅对象语法可用
预防方法:始终使用 useQuery({ queryKey, queryFn, ...options })
之前(v4):
useQuery(['todos'], fetchTodos, { staleTime: 5000 })
之后(v5):
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 5000
})
问题 #2:已移除查询回调
错误:回调不执行,出现 TypeScript 错误
来源:v5 破坏性变更
发生原因:查询中已移除 onSuccess、onError、onSettled(变更中仍可使用)
预防方法:使用 useEffect 处理副作用,或将逻辑移至变更回调
之前(v4):
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
onSuccess: (data) => {
console.log('Todos loaded:', data)
},
})
之后(v5):
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
if (data) {
console.log('Todos loaded:', data)
}
}, [data])
问题 #3:状态从 Loading 改为 Pending
错误:UI 显示错误的加载状态
来源:v5 迁移:isLoading 已重命名
发生原因:status: 'loading' 已重命名为 status: 'pending',isLoading 的含义也已改变
预防方法:初始加载使用 isPending,“等待中且正在获取”使用 isLoading
之前(v4):
const { data, isLoading } = useQuery(...)
if (isLoading) return <div>Loading...</div>
之后(v5):
const { data, isPending, isLoading } = useQuery(...)
if (isPending) return <div>Loading...</div>
// isLoading = isPending && isFetching (fetching for first time)
问题 #4:cacheTime → gcTime
错误:cacheTime is not a valid option
来源:v5 迁移:gcTime
原因:重命名以更准确地体现“垃圾回收时间”
预防方法:使用 gcTime 替代 cacheTime
之前(v4):
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
cacheTime: 1000 * 60 * 60,
})
之后(v5):
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
gcTime: 1000 * 60 * 60,
})
问题 #5:useSuspenseQuery + enabled
错误:类型错误,enabled 选项不可用
来源:GitHub 讨论 #6206
原因:Suspense 保证数据可用,因此不能有条件地禁用
预防方法:使用条件渲染,而非 enabled 选项
之前(v4/不正确):
useSuspenseQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
enabled: !!id, // ❌ Not allowed
})
之后(v5/正确):
// 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>
}
问题 #6:必须提供 initialPageParam
错误:initialPageParam is required 类型错误
来源:v5 迁移:无限查询
原因:v4 将 undefined 作为第一个 pageParam 传递,而 v5 要求提供明确的值
预防方法:始终为无限查询指定 initialPageParam
之前(v4):
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam = 0 }) => fetchProjects(pageParam),
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
之后(v5):
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam }) => fetchProjects(pageParam),
initialPageParam: 0, // ✅ Required
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
问题 #7:已移除 keepPreviousData
错误:keepPreviousData is not a valid option
来源:v5 迁移:placeholderData
原因:已替换为更灵活的 placeholderData 函数
预防方法:使用 placeholderData: keepPreviousData 助手
之前(v4):
useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
keepPreviousData: true,
})
之后(v5):
import { keepPreviousData } from '@tanstack/react-query'
useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
placeholderData: keepPreviousData,
})
问题 #8:TypeScript 错误类型默认值
错误:错误处理时出现类型错误
来源:v5 迁移:错误类型
原因:v4 使用 unknown,v5 默认使用 Error 类型
预防方法:如果抛出的不是 Error 类型,请明确指定错误类型
之前(v4 - 错误类型为 unknown):
const { error } = useQuery({
queryKey: ['data'],
queryFn: async () => {
if (Math.random() > 0.5) throw 'custom error string'
return data
},
})
// error: unknown
之后(v5 - 指定自定义错误类型):
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)
问题 #9:流式服务器组件水合错误
错误:Hydration failed because the initial UI does not match what was rendered on the server
来源:GitHub 问题 #9642
影响范围:使用流式 SSR(void 预取模式)的 v5.82.0+
原因:发生竞态条件,hydrate() 同步完成解析,但 query.fetch() 会创建异步重试器,导致服务器端与客户端的 isFetching/isStale 不一致
预防方法:使用 useSuspenseQuery 和流式预取时,不要基于 fetchStatus 进行条件渲染;或者等待预取完成,而不要使用 void 模式
之前(会导致水合错误):
// 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 />}</>;
之后(临时解决方案):
// 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 isFetching
状态:已知问题,维护者正在调查。需要在 useSyncExternalStore 中实现 getServerSnapshot。
问题 #10:预取时 useQuery 水合错误
错误:水合期间文本内容不匹配
来源:GitHub 问题 #9399
影响范围:使用服务器端预取的 v5.x
原因:tryResolveSync 会检测 RSC 负载中已解析的 Promise,并在水合期间同步提取数据,从而绕过正常的 pending 状态
预防方法:SSR 时使用 useSuspenseQuery 替代 useQuery,或者避免基于 isLoading 进行条件渲染
之前(会导致水合错误):
// 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
}
之后(临时解决方案):
// Use useSuspenseQuery instead
function Todos() {
const { data } = useSuspenseQuery({ queryKey: ['todos'], queryFn: fetchTodos });
return <div>{data.length} todos</div>;
}
状态:“在我的 OSS 待修复事项列表中排在最前面”——维护者 Ephem(2025 年 11 月)。需要在 useSyncExternalStore 中实现 getServerSnapshot。
问题 #11:Errored 查询未遵循 refetchOnMount
错误:尽管设置了 refetchOnMount: false,查询仍会在挂载时重新获取
来源:GitHub Issue #10018
影响版本:v5.90.16+
原因:没有数据的 Errored 查询始终会被视为过期。这是有意为之,以避免错误状态永久显示
预防方法:使用 retryOnMount: false 代替(或与)refetchOnMount: false
修改前(尽管设置了选项,仍会重新获取):
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 mounts
修改后(正确):
const { data, error } = useQuery({
queryKey: ['data'],
queryFn: failingFetch,
refetchOnMount: false,
retryOnMount: false, // ✅ Prevents refetch on mount for errored queries
retry: 0,
});
状态:已记录的行为(有意为之)。retryOnMount 这个名称略有误导——它控制 Errored 查询是否会在挂载时触发新的获取,而非自动重试。
问题 #12:Mutation 回调签名的破坏性变更(v5.89.0)
错误:Mutation 回调中出现 TypeScript 错误
来源:GitHub Issue #9660
影响版本:v5.89.0+
原因:在 variables 和 context 之间添加了 onMutateResult 参数,导致回调签名从 3 个参数变为 4 个
预防方法:更新所有 Mutation 回调,使其接受 4 个参数而非 3 个
修改前(v5.88 及更早版本):
useMutation({
mutationFn: addTodo,
onError: (error, variables, context) => {
// context is now onMutateResult, missing final context param
},
onSuccess: (data, variables, context) => {
// Same issue
}
});
修改后(v5.89.0+):
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
}
});
注意:如果不使用 onMutate,onMutateResult 参数将为 undefined。此破坏性变更是在补丁版本中引入的。
问题 #13:只读查询键导致部分匹配失效(v5.90.8)
错误:Type 'readonly ["todos", string]' is not assignable to type '["todos", string]'
来源:GitHub Issue #9871 | 已在 PR #9872 中修复
影响版本:仅 v5.90.8(已在 v5.90.9 中修复)
原因:部分查询匹配导致只读查询键(使用 as const)的 TypeScript 类型失效
预防方法:升级到 v5.90.9+,或在必须使用 v5.90.8 时进行类型断言
修改前(v5.90.8 - TypeScript 错误):
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]
});
}
});
修改后(v5.90.9+):
// Works correctly with readonly types
queryClient.invalidateQueries({
queryKey: todoQueryKey('123') // ✅ No type error
});
状态:已在 v5.90.9 中修复。使用 openapi-react-query 等代码生成器(会生成只读查询键)的用户尤其容易受到影响。
问题 #14:useMutationState 丢失类型推断
错误:mutation.state.variables 的类型为 unknown,而非实际类型
来源:GitHub Issue #9825
影响版本:所有 v5.x 版本
原因:模糊的 Mutation 键匹配无法保证类型推断(与 queryClient.getQueryCache().find() 存在相同问题)
预防方法:在 select 回调中显式进行类型转换
修改前(类型推断不起作用):
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
},
});
修改后(显式类型转换):
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>
状态:模糊匹配的已知限制。暂无修复计划。
问题 #15:StrictMode 下使用 fetchQuery 时查询被取消
错误:将 fetchQuery() 与 useQuery 搭配使用时出现 CancelledError
来源:GitHub Issue #9798
影响范围:仅开发环境(React StrictMode)
原因:StrictMode 会导致组件重复挂载和卸载。当 useQuery 卸载且它是最后一个观察者时,即使 fetchQuery() 也在运行,查询仍会被取消
预防方法:这是预期的、仅发生于开发环境的行为,不会影响生产环境
示例:
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
}
解决方法:
// Keep query observed with staleTime
const { data } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
staleTime: Infinity, // Keeps query active
});
状态:这是预期的 StrictMode 行为,并非 Bug。生产构建不受影响。
问题 #16:invalidateQueries 仅重新获取活跃查询
错误:调用 invalidateQueries() 后,非活跃查询没有重新获取
来源:GitHub Issue #9531
影响范围:所有 v5.x 版本
原因:文档具有误导性——默认情况下,invalidateQueries() 仅重新获取“活跃”查询,而非“所有”查询
预防方法:使用 refetchType: 'all' 强制重新获取非活跃查询
默认行为:
// Only active queries (currently being observed) will refetch
queryClient.invalidateQueries({ queryKey: ['todos'] });
重新获取非活跃查询:
queryClient.invalidateQueries({
queryKey: ['todos'],
refetchType: 'all' // Refetch active AND inactive
});
状态:文档已修正,明确说明“活跃”查询。这是预期行为。
社区提示
注意:这些提示来自社区专家和维护者的博客。请根据你使用的版本进行核实。
提示:多个监听器共用查询选项
来源:TkDodo's Blog - API Design Lessons | 可信度:高 适用版本:v5.27.3+
多个组件使用同一查询但设置了不同选项(例如 staleTime)时,“后写入者胜出”规则适用于后续获取,但当前正在进行的查询仍使用其原始选项。如果组件在不同时间挂载,可能会导致意外行为。
意外行为示例:
// 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
});
}
推荐做法:
// Write options as functions that reference latest values
const getStaleTime = () => shouldUseLongCache ? 60000 : 5000;
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: getStaleTime(), // Evaluated on each render
});
提示:refetch() 不适用于参数变更
来源:Avoiding Common Mistakes with TanStack Query | 可信度:高
refetch() 函数只应用于使用相同参数刷新(例如手动点击“重新加载”按钮)。对于新参数(筛选条件、页码、搜索词等),应将它们加入查询键。
反模式:
// ❌ 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 - 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 state
何时使用 refetch():
// ✅ Manual refresh of same data (refresh button)
const { data, refetch } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
});
<button onClick={() => refetch()}>Refresh</button> // Same parameters
关键模式
依赖查询(查询 B 等待查询 A):
const { data: posts } = useQuery({
queryKey: ['users', userId, 'posts'],
queryFn: () => fetchUserPosts(userId),
enabled: !!user, // Wait for user
})
并行查询(同时获取多项数据):
const results = useQueries({
queries: ids.map(id => ({ queryKey: ['todos', id], queryFn: () => fetchTodo(id) })),
})
预取(悬停时预加载):
queryClient.prefetchQuery({ queryKey: ['todo', id], queryFn: () => fetchTodo(id) })
无限滚动(useInfiniteQuery):
useInfiniteQuery({
queryKey: ['todos', 'infinite'],
queryFn: ({ pageParam }) => fetchTodosPage(pageParam),
initialPageParam: 0, // Required in v5
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
取消查询(queryKey 变更时自动取消):
queryFn: async ({ signal }) => {
const res = await fetch(`/api/todos?q=${search}`, { signal })
return res.json()
}
数据转换(select):
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