用途
TanStack Query v5(React Query)用于异步操作、数据获取、缓存和状态管理。适用于获取服务端数据、管理异步操作、变更(mutations)、无限查询和乐观更新。
不涵盖:URL 状态管理(应使用 nuqs)、分组同步状态(应使用 ahooks.useSetState)、localStorage 持久化(应使用 ahooks.useLocalStorageState)。
前提假设
- TanStack Query v5+
- React 18+,支持 hooks
- TypeScript v5+(用于类型推断)
- 在共享查询模块中使用手写的查询键元组(query-key-factory 为可选)
- 在应用根部配置 QueryClientProvider
原则
- 任何能从缓存或状态管理中受益的异步操作都应使用 TanStack Query
- 优先使用手写的查询键元组,与 hooks 同置或放在共享的 queries/ 模块中
- 绝不手动管理 isLoading、error 或 isError 状态(由 hooks 提供)
- queryFn 可以是任何返回 Promise 的函数(不限于 HTTP 调用)
- 所有 TanStack 特性对任何数据源都同样有效:缓存、去重、后台重新获取、stale-while-revalidate
约束
必须:
- 使用稳定、分层的查询键(手写元组或可选的 query-key-factory)
- 使用 TanStack Query hooks 处理异步操作(绝不手动管理加载/错误状态)
应该:
- 对任何能从缓存中受益的异步操作使用 TanStack Query(HTTP、localStorage 读取、本地计算、文件操作)
- 将查询逻辑提取到自定义 hooks 中以实现可复用性
- 将多个查询组合成内聚的 hooks
- 使用 TypeScript 泛型实现类型安全的查询:useQuery<ResponseType>(...)
- 在 provider 级别配置 QueryClient 默认值(retry、staleTime)
- 当项目已标准化使用 @lukemorales/query-key-factory 时使用它
避免:
- 在失效操作中硬编码不相关的查询键,且没有共享前缀
- 手动管理加载/错误状态(使用 hook 提供的状态)
- 用于 URL 可共享状态(应改用 nuqs)
- 用于分组同步状态(应改用 ahooks.useSetState)
- 用于没有缓存需求的一次性 promise(在事件处理器中使用普通 async/await)
交互
- 与 nuqs-v2 互补,用于 URL 状态管理(查询可以依赖 URL 参数)
- 与 ahooks-v3 互补,用于同步状态(useSetState 用于表单状态,useLocalStorageState 用于持久化)
- 与 OpenAPI 生成的 API 客户端配合使用;React Query hooks 是针对这些客户端手写的
- 是状态管理决策树的一部分(参见 React 规则)
模式
手写查询键
同置、稳定的查询键:
export const userKeys = { all: ['users'] as const, detail: (id: string) => [...userKeys.all, id] as const, list: (filters?: Filters) => [...userKeys.all, 'list', filters] as const, }
// 用法 import { useQuery } from '@tanstack/react-query' import { userKeys } from '@/queries/users'
const { data, isLoading, error } = useQuery({ queryKey: userKeys.detail(userId), queryFn: () => fetchUser(userId), })
可选的查询键工厂
当项目已使用 @lukemorales/query-key-factory 时:
import { createQueryKeys } from '@lukemorales/query-key-factory'
export const users = createQueryKeys('users', { detail: (id: string) => ({ queryKey: [id], queryFn: () => fetchUser(id), }), })
通用 queryFn 模式
TanStack Query 接受任何异步 queryFn——不仅仅是 HTTP 调用:
// 服务端数据获取(传统) const { data } = useQuery({ queryKey: ['users', userId], queryFn: () => fetchUser(userId), })
// 本地计算(无网络) const { data } = useQuery({ queryKey: ['fibonacci', n], queryFn: () => computeFibonacci(n), })
// localStorage 读取 const { data: settings } = useQuery({ queryKey: ['user-settings'], queryFn: () => Promise.resolve( JSON.parse(localStorage.getItem('settings') || '{}') ), })
// 文件操作 const { data } = useQuery({ queryKey: ['file-content', path], queryFn: () => readFile(path), })
何时使用:任何能从自动缓存、过期管理或重复执行中受益的异步操作
何时不使用:没有缓存需求的一次性 promise(在事件处理器中使用普通 async/await)
变更模式
带自动失效的变更:
import { useMutation, useQueryClient } from '@tanstack/react-query' import { userKeys } from '@/queries/users'
function useCreateUser() { const queryClient = useQueryClient()
return useMutation({ mutationFn: async (data: CreateUserInput) => { const response = await createUser(data) return response }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: userKeys.all }) }, }) }
无限查询模式
带无限滚动的分页:
import { useInfiniteQuery } from '@tanstack/react-query' import { users } from '@/queries/users'
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, } = useInfiniteQuery({ queryKey: ['users', 'infinite'], queryFn: ({ pageParam = 0 }) => fetchUsers({ page: pageParam }), getNextPageParam: (lastPage, pages) => lastPage.nextPage, initialPageParam: 0, })
QueryClient 配置
在 provider 级别配置默认值:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { useState } from 'react'
function Providers({ children }: { children: ReactNode }) { const [queryClient] = useState(() => new QueryClient({ defaultOptions: { queries: { staleTime: 60 * 1000, // 1 分钟 retry: 1, refetchOnWindowFocus: false, }, }, }))
return ( <QueryClientProvider client={queryClient}> {children} </QueryClientProvider> ) }
手写 Hook 模式
将生成的 OpenAPI 客户端包装在自定义 hooks 中:
import { useQuery } from '@tanstack/react-query' import { apiClient } from '@/lib/api-client'
export function useHealthCheck() { return useQuery({ queryKey: ['health'], queryFn: () => apiClient.GET('/health').then(r => r.data), }) }
自定义 Hook 模式
将查询逻辑提取到可复用的 hooks 中:
// hooks/useUser.ts import { useQuery } from '@tanstack/react-query' import { userKeys } from '@/queries/users'
export function useUser(userId: string) { return useQuery({ queryKey: userKeys.detail(userId), queryFn: () => fetchUser(userId), }) }
状态管理决策树
- URL 可共享状态 → 使用 nuqs(过滤器、搜索、标签页、分页)
- 不在 URL 中的分组状态 → 使用 ahooks.useSetState(表单状态、游戏引擎、临时 UI)
- 异步操作 → 使用 TanStack Query(数据获取、变更、缓存)
- localStorage 持久化 → 使用 ahooks.useLocalStorageState(偏好设置、设置)
- 简单的独立状态 → 使用 useState(少见,优先考虑其他选项)
使用 TanStack Query 的场景:服务端数据获取、变更、乐观更新、后台重新获取、缓存、任何能从状态管理中受益的异步操作
不使用 TanStack Query 的场景:URL 可共享状态(使用 nuqs)、分组同步状态(使用 ahooks.useSetState)、没有缓存需求的一次性 promise
参考
- TanStack Query 文档 - 官方文档
- Query Key Factory - 查询键工厂库
- React 规则 - 状态管理决策树和模式
- ahooks - 用于同步状态的工具 hooks
- OpenAPI 集成 - 生成的客户端模式