名称: tanstack-query 描述: TanStack Query v5(React Query)服务器状态管理。用于数据获取、缓存、突变,或遇到 v4 迁移、陈旧数据、无效错误。
关键词: TanStack Query, React Query, useQuery, useMutation, useInfiniteQuery, useSuspenseQuery, QueryClient, QueryClientProvider, 数据获取, 服务器状态, 缓存, staleTime, gcTime, 查询无效, 预取, 乐观更新, 突变, 查询键, 查询函数, 错误边界, suspense, React Query DevTools, v5 迁移, v4 到 v5, 请求瀑布, 后台重新获取, cacheTime 重命名, loading 状态重命名, pending 状态, initialPageParam 必填, keepPreviousData 移除, placeholderData, 查询回调移除, onSuccess 移除, onError 移除, 对象语法必填 许可证: MIT
TanStack Query (React Query) v5
状态: 生产就绪 ✅ 最后更新: 2025-12-09 依赖: React 18.0+(推荐 18.3+), TypeScript 4.9+(首选 5.x) 最新版本: @tanstack/react-query@5.90.12, @tanstack/react-query-devtools@5.91.1, @tanstack/eslint-plugin-query@5.91.2
快速开始(5分钟)
1. 安装依赖
# 选择包管理器
pnpm add @tanstack/react-query@latest @tanstack/react-query-devtools@latest
# 或
npm install @tanstack/react-query@latest @tanstack/react-query-devtools@latest
# 或
bun add @tanstack/react-query@latest @tanstack/react-query-devtools@latest
为什么重要:
- TanStack Query v5 需要 React 18+(使用 useSyncExternalStore)
- DevTools 对于调试查询和突变至关重要
- v5 有从 v4 的破坏性更改 - 使用最新版本以获取所有修复
2. 设置 QueryClient Provider
// 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包裹整个应用 - 配置
staleTime以避免过度重新获取(默认为 0) - 使用
gcTime(不是cacheTime- v5 中重命名) - DevTools 应在 provider 内
了解默认值(v5):
staleTime: 0→ 数据立即陈旧,因此在挂载/聚焦时重新获取,除非您提高它gcTime: 5 * 60 * 1000→ 非活动数据在 5 分钟后被垃圾回收retry: 3在浏览器中,retry: 0在服务器上refetchOnWindowFocus: true和refetchOnReconnect: truenetworkMode: 'online'(请求在离线时暂停)。切换到'always'用于 SSR/预取,不希望取消
3. 创建第一个查询
// 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,
})
}
// 在组件中使用:
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(不是isLoading- 现在意思是“pending AND fetching”) - 总是在 queryFn 中抛出错误以进行适当的错误处理
- QueryKey 应为数组以保持一致的缓存键
4. 创建第一个突变
// 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: () => {
// 使查询无效并重新获取 todos
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 ? '添加中...' : '添加 Todo'}
</button>
</form>
)
}
为什么有效:
- 突变使用回调(
onSuccess,onError,onSettled) - 查询不使用 invalidateQueries触发后台重新获取- 突变默认不缓存(正确行为)
7步设置过程
步骤 1: 安装依赖
# 核心库(必需)
pnpm add @tanstack/react-query
# DevTools(强烈推荐用于开发)
pnpm add -D @tanstack/react-query-devtools
# 可选: ESLint 插件用于最佳实践
pnpm add -D @tanstack/eslint-plugin-query
包角色:
@tanstack/react-query- 核心 React hooks 和 QueryClient@tanstack/react-query-devtools- 可视化调试器(仅开发,可摇树优化)@tanstack/eslint-plugin-query- 捕获常见错误
版本要求:
- React 18.0 或更高(使用
useSyncExternalStore) - TypeScript 5.2+ 以获取最佳类型推断(可选但推荐)
步骤 2: 配置 QueryClient
// 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(默认): 数据立即陈旧,挂载/聚焦时重新获取1000 * 60 * 5: 数据新鲜 5 分钟,在此期间不重新获取Infinity: 数据永远不陈旧,仅手动无效
gcTime: 未使用数据在缓存中停留的时间1000 * 60 * 5(默认): 5 分钟Infinity: 永不垃圾回收(内存泄漏风险)
何时重新获取:
refetchOnWindowFocus: true- 适用于频繁变化的数据(股票价格)refetchOnWindowFocus: false- 适用于稳定数据或开发期间refetchOnMount: true- 确保组件挂载时有新鲜数据refetchOnReconnect: true- 网络重新连接后重新获取
步骤 3: 用 Provider 包裹应用
// 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 hooks 的组件
- DevTools 必须在 provider 内
- 整个应用仅一个 QueryClient 实例
DevTools 配置:
initialIsOpen={false}- 默认折叠buttonPosition="bottom-right"- 显示切换按钮的位置- 在生产构建中自动移除(可摇树优化)
高级设置(步骤 4-7)
详细模式: 实现自定义查询钩子、带乐观更新的突变、DevTools 配置或错误边界时,加载 references/advanced-setup.md。
快速摘要:
步骤 4: 自定义查询钩子 - 使用 queryOptions 工厂用于可重用模式。创建封装 API 调用的自定义钩子。
步骤 5: 突变 - 使用 useMutation 和 onSuccess 使查询无效。对于即时 UI 反馈,使用 onMutate/onError/onSettled 模式实现乐观更新。
步骤 6: DevTools - 已在步骤 3 中包含。参考中提供高级选项用于自定义。
步骤 7: 错误边界 - 使用带有 React 错误边界的 QueryErrorResetBoundary。配置 throwOnError 选项用于全局 vs 局部错误处理。
关键规则
总是做
✅ 对所有钩子使用对象语法
// v5 仅支持此:
useQuery({ queryKey, queryFn, ...options })
useMutation({ mutationFn, ...options })
✅ 使用数组查询键
queryKey: ['todos'] // 列表
queryKey: ['todos', id] // 详情
queryKey: ['todos', { filter }] // 过滤
✅ 适当配置 staleTime
staleTime: 1000 * 60 * 5 // 5 分钟 - 防止过度重新获取
✅ 使用 isPending 用于初始加载状态
if (isPending) return <Loading />
// isPending = 尚无数据 AND 获取中
✅ 在 queryFn 中抛出错误
if (!response.ok) throw new Error('失败')
✅ 突变后使查询无效
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
}
✅ 使用 queryOptions 工厂用于可重用模式
const opts = queryOptions({ queryKey, queryFn })
useQuery(opts)
useSuspenseQuery(opts)
prefetchQuery(opts)
✅ 使用 gcTime(非 cacheTime)
gcTime: 1000 * 60 * 60 // 1 小时
✅ 了解状态标志
isPending // 尚无数据,获取进行中
isFetching // 任何获取进行中(包括重新获取)
isRefetching // 专门重新获取(数据已缓存)
isLoadingError // 初始加载失败
isPaused // networkMode 暂停(例如,离线)
isFetchingNextPage // useInfiniteQuery 加载更多
绝不做
❌ 绝不使用 v4 数组/函数语法
// v4(v5 中移除):
useQuery(['todos'], fetchTodos, options) // ❌
// v5(正确):
useQuery({ queryKey: ['todos'], queryFn: fetchTodos }) // ✅
❌ 绝不使用查询回调(onSuccess, onError, onSettled 在查询中)
// 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: () => {}, // ✅ 仍适用于突变
})
❌ 绝不使用已弃用选项
// v5 中弃用:
cacheTime: 1000 // ❌ 改用 gcTime
isLoading: true // ❌ 含义更改,使用 isPending
keepPreviousData: true // ❌ 改用 placeholderData
onSuccess: () => {} // ❌ 从查询中移除
useErrorBoundary: true // ❌ 改用 throwOnError
❌ 绝不假设 isLoading 意为“尚无数据”
// v5 更改了此:
isLoading = isPending && isFetching // ❌ 现在意为“pending AND fetching”
isPending = 尚无数据 // ✅ 用于初始加载
❌ 绝不忘记 infinite queries 的 initialPageParam
// v5 需要此:
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam }) => fetchProjects(pageParam),
initialPageParam: 0, // ✅ v5 中必需
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
❌ 绝不将 enabled 与 useSuspenseQuery 使用
// 不允许:
useSuspenseQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
enabled: !!id, // ❌ 不适用于 suspense
})
// 改用条件渲染:
{id && <TodoComponent id={id} />}
错误预防
此技能防止 8+ 记录的 v5 迁移问题。最关键错误包括:
- 对象语法必需(v4 函数重载移除)
- 查询回调移除(onSuccess/onError/onSettled)
isPendingvsisLoading状态更改cacheTime重命名为gcTimeinitialPageParam必需用于 infinite querieskeepPreviousData替换为placeholderData
完整错误目录及前后示例: 遇到错误或调试 v5 迁移问题时,加载 references/top-errors.md。
项目配置
基本配置文件: package.json, tsconfig.json, .eslintrc.cjs
关键要求:
- React 18.3.1+(使用 useSyncExternalStore)
- 推荐 TypeScript 严格模式
- ESLint 插件捕获 v4→v5 迁移错误
完整配置模板: 设置新项目或排除构建/类型错误时,加载 references/configuration-files.md。
常见模式
模式 1: 依赖查询
// 先获取用户,然后获取用户的帖子
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 的数据
模式 2: 使用 useQueries 并行查询
// 并行获取多个 todos
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>错误加载 todos</div>
return (
<ul>
{results.map((result, i) => (
<li key={ids[i]}>{result.data?.title}</li>
))}
</ul>
)
}
何时使用: 并行获取多个独立查询
模式 3: 预取
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>
)
}
何时使用: 用户导航前预加载数据(悬停、挂载)
模式 4: 无限滚动
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>
)
}
何时使用: 分页列表带无限滚动
模式 5: 查询取消
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="搜索 todos..."
/>
{data && (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)}
</div>
)
}
如何工作:
- 当 queryKey 更改时,先前查询自动取消
- 传递
signal给 fetch 以进行适当清理 - 浏览器中止待处理获取请求
模式 6: 后台获取指示器
const { data, isFetching, isRefetching } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 1000 * 60 * 5,
})
return (
<div>
{isFetching && <Spinner label={isRefetching ? '刷新中…' : '加载中…'} />}
<TodoList data={data} />
</div>
)
为什么: isFetching 在后台重新获取期间保持 true,因此您可以显示细微的“刷新中”徽章而不丢失缓存数据。
使用捆绑资源
模板 (templates/)
完整、可复制的代码示例:
package.json- 带确切版本的依赖query-client-config.ts- 带最佳实践的 QueryClient 设置provider-setup.tsx- 带 QueryClientProvider 的应用包装器use-query-basic.tsx- 基本 useQuery 钩子模式use-mutation-basic.tsx- 基本 useMutation 钩子use-mutation-optimistic.tsx- 乐观更新模式use-infinite-query.tsx- 无限滚动模式custom-hooks-pattern.tsx- 可重用查询钩子带 queryOptionserror-boundary.tsx- 带查询重置的错误边界devtools-setup.tsx- DevTools 配置
示例使用:
# 复制查询客户端配置
cp ~/.claude/skills/tanstack-query/templates/query-client-config.ts src/lib/
# 复制 provider 设置
cp ~/.claude/skills/tanstack-query/templates/provider-setup.tsx src/main.tsx
# 或运行引导助手(安装依赖 + 复制核心文件):
./scripts/example-script.sh . pnpm
参考 (references/)
深度文档,需要时加载:
advanced-setup.md- 自定义钩子、突变、乐观更新、DevTools、错误边界configuration-files.md- 完整 package.json, tsconfig.json, .eslintrc.cjs 模板v4-to-v5-migration.md- 完整 v4 → v5 迁移指南best-practices.md- 请求瀑布、缓存策略、性能common-patterns.md- 可重用查询、乐观更新、无限滚动official-guides-map.md- 何时打开每个官方文档及其覆盖内容typescript-patterns.md- 类型安全、泛型、类型推断testing.md- 使用 MSW、React Testing Library 测试top-errors.md- 所有 8+ 错误带解决方案
示例 (examples/)
examples/README.md- 前 10 场景索引带官方链接basic.tsx- 最小列表查询basic-graphql-request.tsx- GraphQL 客户端 + selectoptimistic-update.tsx- onMutate 快照/回滚pagination.tsx- 分页列表带 placeholderDatainfinite-scroll.tsx- useInfiniteQuery + IntersectionObserverprefetching.tsx- 导航前悬停预取suspense.tsx- useSuspenseQuery + 边界default-query-function.ts- 使用 queryKey 的全局 fetchernextjs-app-router.tsx- App Router 预取 + 水合(networkMode: 'always')react-native.tsx- 离线优先带 AsyncStorage 持久器
Claude 应何时加载这些:
advanced-setup.md- 实现自定义查询钩子、突变或错误边界时configuration-files.md- 设置新项目或排除构建/类型错误时v4-to-v5-migration.md- 迁移现有 React Query v4 项目时best-practices.md- 优化性能或避免瀑布时common-patterns.md- 实现特定功能(无限滚动等)时typescript-patterns.md- 处理 TypeScript 错误或类型推断时testing.md- 为使用 TanStack Query 的组件编写测试时top-errors.md- 遇到主 SKILL.md 未涵盖的错误时
高级主题
使用 select 进行数据转换
// 仅订阅数据的特定切片
function TodoCount() {
const { data: count } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (data) => data.length, // 仅计数更改时重新渲染
})
return <div>总计 todos: {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>
)
}
好处:
- 组件仅在选择的数据更改时重新渲染
- 减少内存使用(组件状态中存储更少数据)
- 保持查询缓存不变(其他组件获取完整数据)
请求瀑布(反模式)
// ❌ 差: 顺序瀑布
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 查询并行运行 = 快!
}
服务器状态 vs 客户端状态
// ❌ 不将 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(主题、身份验证令牌)
平台和集成说明
- React Native: 与 web 相同工作。使用
@tanstack/query-async-storage-persister持久缓存到 AsyncStorage;避免窗口聚焦重新获取逻辑。DevTools 面板本地不可用 — 使用 Flipper 或暴露日志。 - GraphQL: 与
graphql-request或 urql 的裸客户端配对。将操作视为普通异步函数;共置片段并使用select将 edges/nodes 映射到扁平形状。 - SSR / Next.js / TanStack Start: 在服务器上使用
dehydrate/HydrationBoundary和在客户端上使用QueryClientProvider。为服务器预取设置networkMode: 'always'以便请求永不暂停。 - Suspense: 优先
useSuspenseQuery用于已使用 Suspense 的路由。不与enabled结合;改用条件渲染。 - 测试: 使用
@testing-library/react+@tanstack/react-query/testing助手和使用 MSW 模拟网络。测试间重置 QueryClient 以避免缓存污染。
依赖
必需:
@tanstack/react-query@5.90.12- 核心库react@18.0.0+- 使用 useSyncExternalStore 钩子react-dom@18.0.0+- React DOM 渲染器
推荐:
@tanstack/react-query-devtools@5.91.1- 可视化调试器(仅开发)@tanstack/eslint-plugin-query@5.91.2- 最佳实践 ESLint 规则typescript@5.2.0+- 用于类型安全和推断
可选:
@tanstack/query-sync-storage-persister- 持久缓存到 localStorage@tanstack/query-async-storage-persister- 持久到 AsyncStorage(React Native)
官方文档
- 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
包版本(验证 2025-12-09)
{
"dependencies": {
"@tanstack/react-query": "^5.90.12"
},
"devDependencies": {
"@tanstack/react-query-devtools": "^5.91.1",
"@tanstack/eslint-plugin-query": "^5.91.2"
}
}
验证:
npm view @tanstack/react-query version→ 5.90.12npm view @tanstack/react-query-devtools version→ 5.91.1npm view @tanstack/eslint-plugin-query version→ 5.91.2- 最后检查: 2025-12-09
生产示例
此技能基于生产中使用的模式:
- 构建时间: ~6 小时研究 + 开发
- 防止的错误: 8(所有记录的 v5 迁移问题)
- 令牌效率: ~65% 节省 vs 手动设置
- 验证: ✅ 所有模式使用 TypeScript 严格模式测试
故障排除
问题: “useQuery is not a function” 或类型错误
解决方案: 确保使用 v5 对象语法:
// ✅ 正确:
useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
// ❌ 错误(v4 语法):
useQuery(['todos'], fetchTodos)
问题: 回调(onSuccess, onError)在查询中不工作
解决方案: v5 中移除。使用 useEffect 或移到突变:
// ✅ 用于查询:
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
if (data) {
// 处理成功
}
}, [data])
// ✅ 用于突变(仍工作):
useMutation({
mutationFn: addTodo,
onSuccess: () => { /* ... */ },
})
问题: isLoading 始终 false,即使初始加载期间
解决方案: 改用 isPending:
const { isPending, isLoading, isFetching } = useQuery(...)
// isPending = 尚无数据
// isLoading = isPending && isFetching
// isFetching = 任何获取进行中
问题: cacheTime 选项不识别
解决方案: v5 中重命名为 gcTime:
gcTime: 1000 * 60 * 60 // 1 小时
问题: useSuspenseQuery 带 enabled 选项给出类型错误
解决方案: enabled 不适用于 suspense。使用条件渲染:
{id && <TodoComponent id={id} />}
问题: 突变后数据不重新获取
解决方案: 在 onSuccess 中使查询无效:
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
}
问题: Infinite query 需要 initialPageParam
解决方案: v5 中始终提供 initialPageParam:
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam }) => fetchProjects(pageParam),
initialPageParam: 0, // 必需
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
问题: keepPreviousData 不工作
解决方案: 替换为 placeholderData:
import { keepPreviousData } from '@tanstack/react-query'
useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
placeholderData: keepPreviousData,
})
完整设置清单
使用此清单验证设置:
- [ ] 安装 @tanstack/react-query@5.90.12+
- [ ] 安装 @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