关于 tanstack-query
面向 React 18+ 与 TypeScript 项目的 TanStack Query v5 开发技能,涵盖查询、变更和缓存配置。重点说明 v5 用法及迁移中容易出错的差异。
主要功能
- 01管理查询与变更
- 02配置缓存和重新获取
- 03支持预取与无限查询
- 04介绍 DevTools 与错误处理
- 05说明 v4 到 v5 的迁移差异
适用场景
- 01为 React 应用接入服务端数据查询
- 02在变更后刷新相关查询
- 03实现分页列表和数据预取
- 04排查 v5 状态标志与选项问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ secondsky/ claude-skills --skill tanstack-query安装后在 Claude Code 中输入/tanstack-query确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
assets/example-template.txtexamples/basic-graphql-request.tsxbasic.tsxdefault-query-function.tsxinfinite-scroll.tsxnextjs-app-router.tsxoptimistic-update.tsxpagination.tsxprefetching.tsxreact-native.tsxREADME.mdsuspense.tsxreferences/advanced-setup.mdbest-practices.mdcommon-patterns.mdconfiguration-files.mdexample-reference.mdofficial-guides-map.mdtesting.mdtop-errors.mdtypescript-patterns.mdv4-to-v5-migration.mdscripts/example-script.shtemplates/custom-hooks-pattern.tsxdevtools-setup.tsxerror-boundary.tsxpackage.jsonprovider-setup.tsxquery-client-config.tsuse-infinite-query.tsx详细介绍
TanStack Query(React Query)v5
状态:生产就绪 ✅ 最后更新:2026-08-03 依赖项:React 18.0+(推荐 18.3+)、TypeScript 4.9+(优先使用 5.x) 最新版本:@tanstack/[email protected]、@tanstack/[email protected]、@tanstack/[email protected]
快速入门(5 分钟)
1. 安装依赖项
# choose your package manager
pnpm add @tanstack/react-query@latest @tanstack/react-query-devtools@latest
# or
npm install @tanstack/react-query@latest @tanstack/react-query-devtools@latest
# or
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 or 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'
// Create a client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 60, // 1 hour (formerly 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'(离线时请求会暂停)。对于不希望取消请求的 SSR/预取场景,切换为'always'。 citeturn1search0turn1search1
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,
})
}
// Usage in component:
function TodoList() {
const { data, isPending, isError, error } = useTodos()
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>
)
}
重要:
- v5 要求使用对象语法:
useQuery({ queryKey, queryFn }) - 使用
isPending(而不是isLoading——现在它表示“待处理且正在获取”) - 始终在 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: () => {
// Invalidate and refetch todos
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
}
// Usage in component:
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 ? 'Adding...' : 'Add Todo'}
</button>
</form>
)
}
这样做的原因:
- 变更使用回调(
onSuccess、onError、onSettled),查询则不使用 invalidateQueries会触发后台重新获取- 变更默认不会缓存(这是正确行为)
7 步设置流程
第 1 步:安装依赖项
# Core library (required)
pnpm add @tanstack/react-query
# DevTools (highly recommended for development)
pnpm add -D @tanstack/react-query-devtools
# Optional: ESLint plugin for best practices
pnpm add -D @tanstack/eslint-plugin-query
软件包的作用:
@tanstack/react-query- React 核心 Hooks 和 QueryClient@tanstack/react-query-devtools- 可视化调试工具(仅开发环境,可进行 Tree Shaking)@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: {
// How long data is considered fresh (won't refetch during this time)
staleTime: 1000 * 60 * 5, // 5 minutes
// How long inactive data stays in cache before garbage collection
gcTime: 1000 * 60 * 60, // 1 hour (v5: renamed from cacheTime)
// Retry failed requests (0 on server, 3 on client by default)
retry: (failureCount, error) => {
if (error instanceof Response && error.status === 404) return false
return failureCount < 3
},
// Refetch on window focus (can be annoying during dev)
refetchOnWindowFocus: false,
// Refetch on network reconnect
refetchOnReconnect: true,
// Refetch on component mount if data is stale
refetchOnMount: true,
},
mutations: {
// Retry mutations on failure (usually don't want this)
retry: 0,
},
},
})
关键配置决策:
staleTime 与 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"- 切换按钮的显示位置- 生产构建时会自动移除(通过 Tree Shaking)
高级设置(步骤 4-7)
详细模式:在实现自定义查询钩子、带乐观更新的变更、DevTools 配置或错误边界时,加载 references/advanced-setup.md。
快速摘要:
步骤 4:自定义查询钩子 - 使用 queryOptions 工厂实现可复用模式。创建封装 API 调用的自定义钩子。
步骤 5:变更 - 使用带有 onSuccess 的 useMutation 使查询失效。如需即时 UI 反馈,可使用 onMutate/onError/onSettled 模式实现乐观更新。
步骤 6:DevTools - 已包含在步骤 3 中。参考资料中提供了可用于自定义的高级选项。
步骤 7:错误边界 - 将 QueryErrorResetBoundary 与 React Error Boundary 搭配使用。配置 throwOnError 选项,以实现全局或局部错误处理。
关键规则
务必做到
✅ 所有钩子都使用对象语法
// 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
✅ 了解各种状态标志
isPending // no data yet, fetch in flight
isFetching // any fetch in flight (including refetch)
isRefetching // refetch specifically (data already cached)
isLoadingError // initial load failed
isPaused // networkMode paused (e.g., offline)
isFetchingNextPage // useInfiniteQuery loading more
切勿这样做
❌ 切勿使用 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,
})
❌ 切勿在 useSuspenseQuery 中使用 enabled
// Not allowed:
useSuspenseQuery({
queryKey: ['todo', id],
queryFn: () => fetchTodo(id),
enabled: !!id, // ❌ Not available with suspense
})
// Use conditional rendering instead:
{id && <TodoComponent id={id} />}
错误预防
此技能可预防 8 种以上有记录的 v5 迁移问题。最关键的错误包括:
- 必须使用对象语法(已移除 v4 函数重载)
- 已移除查询回调(onSuccess/onError/onSettled)
isPending与isLoading的状态变更cacheTime更名为gcTime- 无限查询必须设置
initialPageParam keepPreviousData已由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:依赖查询
// Fetch user, then fetch user's posts
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, // Only fetch posts after user is loaded
})
if (!user) return <div>Loading user...</div>
if (!posts) return <div>Loading posts...</div>
return (
<div>
<h1>{user.name}</h1>
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
)
}
适用场景:查询 B 依赖查询 A 的数据
模式 2:使用 useQueries 并行查询
// Fetch multiple todos in parallel
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>Loading...</div>
if (isError) return <div>Error loading 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 minutes
})
}
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>Loading more...</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, // Only search if 3+ characters
})
return (
<div>
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search todos..."
/>
{data && (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)}
</div>
)
}
工作原理:
- queryKey 变更时,之前的查询会自动取消
- 将
signal传递给 fetch,以便正确清理 - 浏览器会中止正在等待的 fetch 请求
模式 6:后台获取指示器
const { data, isFetching, isRefetching } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 1000 * 60 * 5,
})
return (
<div>
{isFetching && <Spinner label={isRefetching ? 'Refreshing…' : 'Loading…'} />}
<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- 使用 queryOptions 的可复用查询钩子error-boundary.tsx- 带查询重置功能的错误边界devtools-setup.tsx- DevTools 配置
使用示例:
# Copy query client config
cp ~/.claude/skills/tanstack-query/templates/query-client-config.ts src/lib/
# Copy provider setup
cp ~/.claude/skills/tanstack-query/templates/provider-setup.tsx src/main.tsx
# Or run the bootstrap helper (installs deps + copies core files):
./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- 使用 placeholderData 的分页列表infinite-scroll.tsx- useInfiniteQuery + IntersectionObserverprefetching.tsx- 导航前在悬停时预取suspense.tsx- useSuspenseQuery + 边界default-query-function.tsx- 使用 queryKey 的全局 fetchernextjs-app-router.tsx- App Router 预取 + 水合(networkMode: 'always')react-native.tsx- 使用 AsyncStorage persister 实现离线优先
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 转换数据
// Only subscribe to specific slice of data
function TodoCount() {
const { data: count } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (data) => data.length, // Only re-render when count changes
})
return <div>Total todos: {count}</div>
}
// Transform data shape
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>
)
}
优点:
- 只有在所选数据发生变化时,组件才会重新渲染
- 减少内存使用量(组件状态中存储的数据更少)
- 保持查询缓存不变(其他组件仍可获取完整数据)
请求瀑布(反模式)
// ❌ BAD: Sequential waterfalls
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,
})
// Each query waits for previous one = slow!
}
// ✅ GOOD: Fetch in parallel when possible
function GoodUserProfile({ userId }: { userId: number }) {
const { data: user } = useQuery({
queryKey: ['users', userId],
queryFn: () => fetchUser(userId),
})
// Fetch posts AND comments in parallel
const { data: posts } = useQuery({
queryKey: ['posts', userId],
queryFn: () => fetchPosts(userId), // Don't wait for user
})
const { data: comments } = useQuery({
queryKey: ['comments', userId],
queryFn: () => fetchUserComments(userId), // Don't wait for posts
})
// All 3 queries run in parallel = fast!
}
服务端状态与客户端状态
// ❌ Don't use TanStack Query for client-only state
const { data: isModalOpen, setData: setIsModalOpen } = useMutation(...)
// ✅ Use useState for client state
const [isModalOpen, setIsModalOpen] = useState(false)
// ✅ Use TanStack Query for server state
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:对于已使用 Suspense 的路由,优先使用
useSuspenseQuery。不要与enabled组合使用;应改为控制渲染。 - 测试:使用
@testing-library/react+@tanstack/react-query/testing辅助工具,并通过 MSW 模拟网络。每次测试之间重置 QueryClient,避免缓存相互影响。
依赖项
必需:
@tanstack/[email protected]- 核心库[email protected]+- 使用 useSyncExternalStore hook[email protected]+- React DOM 渲染器
推荐:
@tanstack/[email protected]- 可视化调试工具(仅开发环境)@tanstack/[email protected]- 用于最佳实践的 ESLint 规则[email protected]+- 用于类型安全和类型推断
可选:
@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
软件包版本(核验于 2026-08-03)
{
"dependencies": {
"@tanstack/react-query": "^5.101.4"
},
"devDependencies": {
"@tanstack/react-query-devtools": "^5.101.4",
"@tanstack/eslint-plugin-query": "^5.101.4"
}
}
核验结果:
npm view @tanstack/react-query version→ 5.101.4npm view @tanstack/react-query-devtools version→ 5.101.4npm view @tanstack/eslint-plugin-query version→ 5.101.4- 上次检查:2026-08-03
生产环境示例
此技能基于以下生产环境实践:
- 构建耗时:研究与开发约 6 小时
- 避免的错误:8 个(均为已记录的 v5 迁移问题)
- Token 效率:相比手动设置节省约 65%
- 验证:✅ 已在 TypeScript 严格模式下测试所有实践
故障排除
问题:"useQuery is not a function" 或类型错误
解决方案:确保使用 v5 对象语法:
// ✅ Correct:
useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
// ❌ Wrong (v4 syntax):
useQuery(['todos'], fetchTodos)
问题:查询中的回调(onSuccess、onError)不起作用
解决方案:这些回调已在 v5 中移除。使用 useEffect 或将逻辑移至 mutations:
// ✅ For queries:
const { data } = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
useEffect(() => {
if (data) {
// Handle success
}
}, [data])
// ✅ For mutations (still work):
useMutation({
mutationFn: addTodo,
onSuccess: () => { /* ... */ },
})
问题:即使在初始加载期间,isLoading 也始终为 false
解决方案:改用 isPending:
const { isPending, isLoading, isFetching } = useQuery(...)
// isPending = no data yet
// isLoading = isPending && isFetching
// isFetching = any fetch in progress
问题:无法识别 cacheTime 选项
解决方案:该选项在 v5 中已更名为 gcTime:
gcTime: 1000 * 60 * 60 // 1 hour
问题:useSuspenseQuery 使用 enabled 选项时报类型错误
解决方案:suspense 不支持 enabled。请使用条件渲染:
{id && <TodoComponent id={id} />}
问题:mutation 后数据没有重新获取
解决方案:在 onSuccess 中使查询失效:
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
}
问题:无限查询要求提供 initialPageParam
解决方案:在 v5 中始终提供 initialPageParam:
useInfiniteQuery({
queryKey: ['projects'],
queryFn: ({ pageParam }) => fetchProjects(pageParam),
initialPageParam: 0, // Required
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
问题:keepPreviousData 不起作用
解决方案:该选项已由 placeholderData 取代:
import { keepPreviousData } from '@tanstack/react-query'
useQuery({
queryKey: ['todos', page],
queryFn: () => fetchTodos(page),
placeholderData: keepPreviousData,
})
完整设置清单
使用此清单验证你的设置:
- 已安装 @tanstack/[email protected]+
- 已安装 @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