关于 tanstack-query
面向使用 React 和 TypeScript 的 TanStack Query v5 用户的参考 Skill,介绍查询与变更模式、缓存、迁移、SSR、离线支持及特定版本问题。
主要功能
- 01查询与变更模式
- 02缓存配置与失效处理
- 03乐观更新与错误处理
- 04SSR、分页和离线指导
- 05v4 到 v5 迁移说明
适用场景
- 01在 React 应用中实现数据获取
- 02将项目从 v4 升级到 v5
- 03配置查询缓存和刷新
- 04排查水合或变更行为问题
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ marcioaltoe/ skills --skill tanstack-query安装后在 Claude Code 中输入/tanstack-query确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents/openai.yamlrules/cache-configuration.mdcache-invalidation.mderr-error-handling.mdinf-infinite-queries.mdmut-basics.mdmut-optimistic-updates.mdoffline-support.mdparallel-queries.mdperf-optimization.mdpf-prefetching.mdqk-query-keys.mdssr-hydration.md详细介绍
TanStack Query v5
版本:@tanstack/react-[email protected] 要求:React 18.0+,TypeScript 4.7+
v5 新功能
- useMutationState — 跨组件跟踪 mutation,无需逐层传递 props
- 简化乐观更新 — 使用待处理 mutation 的
variables,无需操作缓存 - throwOnError — 从
useErrorBoundary重命名而来 - networkMode — 支持离线/PWA(
online|always|offlineFirst) - 支持 combine 的 useQueries — 将并行查询结果合并为单个对象
- infiniteQueryOptions — 用于无限查询的类型安全工厂(与
queryOptions对应) - maxPages — 限制无限查询在缓存中的页面数量(需要双向分页)
- Mutation 回调签名变更(v5.89+) —
onError/onSuccess/onSettled现在接收 4 个参数(新增onMutateResult)
快速设置
npm install @tanstack/react-query@latest @tanstack/react-query-devtools@latest
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>
)
}
统一 Devtools(建议与多个 TanStack 库搭配使用)
如果同时使用 Query + Router(或其他 TanStack 库),请使用统一的 TanStackDevtools 外壳,而不是单独的 devtools 组件:
npm install -D @tanstack/react-devtools
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>
)
}
在 TanStackDevtools 内嵌时,请使用 *Panel 变体(ReactQueryDevtoolsPanel、TanStackRouterDevtoolsPanel)。
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'] })
},
})
}
规则类别
| 优先级 | 类别 | 规则文件 | 影响 |
|---|---|---|---|
| 严重 | 查询键 | rules/qk-query-keys.md | 防止缓存错误和数据不一致 |
| 严重 | 缓存 | rules/cache-configuration.md | 优化性能和数据新鲜度 |
| 高 | 失效 | rules/cache-invalidation.md | 确保过期数据得到正确刷新 |
| 高 | Mutations | rules/mut-basics.md | 确保写入后的数据完整性 |
| 高 | 乐观更新 | rules/mut-optimistic-updates.md | mutation 期间保持 UI 响应灵敏 |
| 高 | 错误处理 | rules/err-error-handling.md | 防止糟糕的用户体验 |
| 中 | 预取 | rules/pf-prefetching.md | 提升感知性能 |
| 中 | 无限查询 | rules/inf-infinite-queries.md | 防止分页错误 |
| 中 | SSR/水合 | rules/ssr-hydration.md | 支持正确的服务端渲染 |
| 中 | 并行查询 | rules/parallel-queries.md | 动态并行获取数据 |
| 低 | 性能 | rules/perf-optimization.md | 减少不必要的重新渲染 |
| 低 | 离线支持 | rules/offline-support.md | 支持离线优先模式 |
关键规则
务必做到
- 所有 Hook 均使用对象语法:
useQuery({ queryKey, queryFn, ...options }) - 查询键使用数组:
['todos']、['todos', id]、['todos', { filter }] - 在 queryFn 中抛出错误:
if (!res.ok) throw new Error('Failed') - 初始加载时使用 isPending:
if (isPending) return <Loading /> - mutation 后使查询失效:
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }) - 使用 queryOptions 工厂:在
useQuery、useSuspenseQuery、prefetchQuery之间复用 - 使用 gcTime(而不是 cacheTime):v5 中已重命名
切勿这样做
- 使用 v4 数组/函数语法:
useQuery(['todos'], fetchTodos)— 已在 v5 中移除 - 使用查询回调:查询中的
onSuccess/onError/onSettled已移除(mutation 中仍可用)——请改用useEffect - 用 isLoading 表示“尚无数据”:其含义在 v5 中已变更——请使用
isPending - 在 useSuspenseQuery 中使用 enabled:不可用——请使用条件渲染
- 使用 keepPreviousData:已移除——请使用
placeholderData: keepPreviousData - 参数变更时调用 refetch():请改为将参数包含在 queryKey 中,查询会自动重新获取
v4→v5 迁移速查表
| v4 | v5 | 备注 |
|---|---|---|
useQuery(['key'], fn, opts) | useQuery({ queryKey, queryFn, ...opts }) | 仅支持对象语法 |
cacheTime | gcTime | 已重命名 |
isLoading(无数据时) | isPending | isLoading = isPending && isFetching |
keepPreviousData: true | placeholderData: keepPreviousData | 导入 keepPreviousData 辅助函数 |
useErrorBoundary | throwOnError | 已重命名 |
查询上的 onSuccess/onError/onSettled | 已移除 | 使用 useEffect 处理副作用 |
pageParam = 0 默认值 | initialPageParam: 0 | 无限查询必需 |
status: 'loading' | status: 'pending' | 已重命名 |
onError(err, vars, ctx) | onError(err, vars, onMutateResult, ctx) | v5.89+ 新增第 4 个参数 |
已知问题(v5.90.x)
- 流式 SSR 水合不匹配——
void prefetchQuery+useSuspenseQuery搭配基于isFetching条件渲染会导致水合错误。解决方法:对预取使用await,或不要基于fetchStatus进行渲染 - 预取时
useQuery水合错误——useQuery+ 服务端预取可能导致服务端与客户端的isLoading不匹配。改用useSuspenseQuery refetchOnMount对出错的查询无效——错误始终处于过期状态。除设置refetchOnMount: false外,还要设置retryOnMount: falseuseMutationState类型——由于模糊匹配,mutation.state.variables的类型为unknown。在select回调中显式进行类型转换invalidateQueries仅重新获取活跃查询——使用refetchType: 'all'以包含非活跃查询- 只读查询键在 v5.90.8 中失效——已在 v5.90.9+ 修复
关键模式
// 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) })
评论
支持 Markdown