查询键与缓存策略
查询键是 TanStack Query 的核心抽象,决定了缓存的颗粒度、失效策略和数据共享。本章详解查询键设计原则、缓存管理 API 和全局配置最佳实践。
查询键的结构
查询键必须是数组(Tuple),元素按 从通用到具体 排列:
// 层级递进的查询键设计
['todos'] // 所有 todos
['todos', todoId] // 单个 todo
['todos', todoId, 'comments'] // 某 todo 的评论
['todos', { status: 'done', sort: 'date' }] // 带筛选参数
💡 最佳实践:把尽可能多的参数放进查询键——包括排序、筛选、分页等。永远不要用
refetch()响应参数变化,应该让参数成为查询键的一部分。
// ❌ 错误:参数通过闭包传递,键不变
function TodoList() {
const [status, setStatus] = useState('all')
const { data, refetch } = useQuery({
queryKey: ['todos'], // 键不包含 status!
queryFn: () => fetchTodos(status), // 但函数使用了 status
})
return <select onChange={e => { setStatus(e.target.value); refetch() }} />
}
// ✅ 正确:status 成为查询键的一部分
function TodoList() {
const [status, setStatus] = useState('all')
const { data } = useQuery({
queryKey: ['todos', { status }], // 键变化自动触发新请求
queryFn: () => fetchTodos(status),
})
return <select onChange={e => setStatus(e.target.value)} />
}查询键的匹配规则
invalidateQueries 和其他缓存操作使用前缀模糊匹配:
// 假设缓存中有这些查询键:
// ['todos', 'list', { status: 'all' }]
// ['todos', 'detail', 1]
// ['todos', 'detail', 2]
// ['users', 'list']
// 模糊匹配:所有以 ['todos'] 开头的都被影响
queryClient.invalidateQueries({ queryKey: ['todos'] })
// → 前三个查询全部失效
// 精确匹配:只匹配完全相同的键
queryClient.invalidateQueries({ queryKey: ['todos', 'detail', 1] })
// → 只有第二个失效
// 不匹配(users 跟 todos 不同)
queryClient.invalidateQueries({ queryKey: ['todos'] })
// → ['users', 'list'] 不受影响
invalidateQueries 的模糊匹配默认是激活的。如需精确匹配,传入 { exact: true }:
queryClient.invalidateQueries({
queryKey: ['todos'],
exact: true, // 只匹配键恰好为 ['todos'] 的查询
})查询键工厂(Query Key Factory)
对于中大型项目,推荐用工厂函数管理查询键,避免散落各处的硬编码:
// src/query/keys.ts
export const todoKeys = {
all: ['todos'] as const,
lists: () => [...todoKeys.all, 'list'] as const,
list: (filters: TodoFilters) => [...todoKeys.lists(), filters] as const,
details: () => [...todoKeys.all, 'detail'] as const,
detail: (id: number) => [...todoKeys.details(), id] as const,
}
// 使用
useQuery({ queryKey: todoKeys.list({ status: 'done' }), queryFn: fetchTodos })
useQuery({ queryKey: todoKeys.detail(1), queryFn: () => fetchTodo(1) })
// 缓存失效
queryClient.invalidateQueries({ queryKey: todoKeys.lists() })
// → 所有列表查询失效,detail 不受影响
💡 最佳实践:用
as const断言让 TypeScript 推断最窄的类型,便于在queryFn中精确解构查询键。
queryOptions 工厂模式
v5 引入了 queryOptions 和 infiniteQueryOptions 辅助函数,将查询键、查询函数和默认选项打包在一起:
import { queryOptions } from '@tanstack/react-query'
// src/query/options/todoOptions.ts
export function todoOptions(id: number) {
return queryOptions({
queryKey: todoKeys.detail(id),
queryFn: () => fetchTodo(id),
staleTime: 5 * 60 * 1000,
})
}
// 在任何地方复用:
useQuery(todoOptions(1))
queryClient.prefetchQuery(todoOptions(2))
queryClient.invalidateQueries({ queryKey: todoKeys.all })这实现了数据层与 UI 层的分离:组件不关心查询的具体实现,只消费 queryOptions 返回的配置。
缓存失效策略
invalidateQueries — 标记为过期
最常用的方法,将匹配到的查询标记为 stale,如有活跃 observer 则触发重取:
// 基础用法
queryClient.invalidateQueries({ queryKey: ['todos'] })
// 参数
queryClient.invalidateQueries({
queryKey: ['todos'],
exact: false, // 默认模糊匹配
refetchType: 'active', // 'active' | 'inactive' | 'all' | 'none'
type: 'all', // 'all' | 'active' | 'inactive'
predicate: (query) => query.state.data?.length > 0, // 自定义条件
})removeQueries — 从缓存删除
queryClient.removeQueries({ queryKey: ['todos', 'list'] })refetchQueries — 强制重取
// 不论数据是否 stale,都重新获取
queryClient.refetchQueries({ queryKey: ['todos'], type: 'active' })cancelQueries — 取消进行中的请求
await queryClient.cancelQueries({ queryKey: ['todos'] })resetQueries — 重置查询状态
// 清除 data/error,重置为初始状态(如同从未请求过)
queryClient.resetQueries({ queryKey: ['todos'] })全局默认配置
在创建 QueryClient 时设定全局默认值,避免在每个 useQuery 中重复:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 3 * 60 * 1000, // 3 分钟新鲜
gcTime: 10 * 60 * 1000, // 10 分钟垃圾回收
retry: 3, // 失败重试 3 次
retryDelay: (attemptIndex) =>
Math.min(1000 * 2 ** attemptIndex, 30000), // 指数退避
refetchOnWindowFocus: true,
refetchOnReconnect: true,
refetchOnMount: true,
// 不重试的 HTTP 状态码
// 配合 retry 自定义函数实现:
retry: (failureCount, error) => {
if (error.statusCode === 404) return false
return failureCount < 3
},
},
mutations: {
retry: 0, // mutation 默认不重试
},
},
})💡 最佳实践:根据应用类型调整
staleTime。Dashboard 类应用可以设 30s~1min;文章/博客类可以设 5min;实时数据保持默认 0。
staleTime 调优指南
| 数据类型 | 推荐 staleTime | 原因 |
|---|---|---|
| 实时数据(股票价格、聊天消息) | 0(默认) |
每次挂载都需要最新数据 |
| 用户资料 | 5 * 60 * 1000(5 分钟) |
变化频率低 |
| 文章/博客内容 | 10 * 60 * 1000(10 分钟) |
很少变化 |
| 下拉选项/配置数据 | 30 * 60 * 1000(30 分钟)或 Infinity |
基本不变 |
| 列表数据(TODO/订单) | 30 * 1000(30 秒) |
需要比较新但容忍短暂延迟 |
// 极端情况:数据永远不会变(配置字典)
useQuery({
queryKey: ['config', 'countries'],
queryFn: fetchCountries,
staleTime: Infinity, // 永远新鲜,只获取一次
})缓存操作的原子性
在同一个回调中执行多次缓存操作时,注意顺序:
// ✅ 正确:先取消再做修改,避免竞态
await queryClient.cancelQueries({ queryKey: ['todos'] })
queryClient.setQueryData(['todos'], (old) => /* 乐观更新 */)
// ... 发起 mutation
// 如果失败,回滚;如果成功,invalidate 获得最终一致性
多实例 QueryClient
v5 支持传入自定义 QueryClient 实例到单个 hook:
const customClient = new QueryClient({
defaultOptions: { queries: { staleTime: Infinity } },
})
// 这个查询使用 customClient,而不是 Provider 中的全局 client
useQuery({
queryKey: ['legacy-data'],
queryFn: fetchOldData,
queryClient: customClient, // v5 新增
})适用场景:微前端中的隔离、第三方组件嵌入等。