前置:Promise、React Hooks。status 回答有没有可用的查询结果,fetchStatus 回答当前请求在做什么,它们不是同一个状态机。
| 情况 | 典型状态 | 界面决策 |
|---|---|---|
| 首次请求进行中 | pending / fetching | 首屏加载 |
| 禁用且无缓存 | pending / idle | 等待条件,而非正在联网 |
| 有结果并后台重取 | success / fetching | 保留数据,附同步提示 |
| 无结果且失败 | error / idle | 错误与重试 |
| 有旧结果但重取失败 | error / idle 且可能仍有 data | 错误提示与保留历史数据 |
自测:若先写 if (data === undefined) return 加载中,首次失败会怎样?错误永远被前一分支截走。请先判断请求状态,并单独设计后台失败。完整演练见基础课。
TanStack Query v5 的三大核心入口:useQuery(声明式读)、useMutation(命令式写)、QueryClient(缓存控制中枢)。本文条目化覆盖参数、返回值与陷阱,教程过程见 Query 基础。
文档信息(用途、难度与维护记录)
| 属性 | 内容 |
|---|---|
| 模块 | 03-tanstack-stack |
| 象限 | 字典 |
| 难度 | ⭐⭐ |
| 标签 | #useQuery #useMutation #QueryClient #API字典 |
| 更新日期 | 2026年9月 |
订阅一条服务端状态缓存。组件挂载/依赖变化时自动取数,同一 queryKey 的多个订阅者共享同一请求。
const result = useQuery({
queryKey: ['todos', 'list', { page: 1 }], // 必填,数组,可序列化
queryFn: ({ signal }) => fetchTodos(signal), // 必填,抛错即 isError
enabled: true, // false 时不自动取数
staleTime: 0, // 数据新鲜期(毫秒),默认 0
gcTime: 5 * 60 * 1000, // 无订阅后的缓存保留期,默认 5 分钟
retry: 3, // 失败重试次数,可为函数
select: (data) => data.filter((t) => !t.completed), // 订阅层派生
placeholderData: (previousData) => previousData, // v5:沿用上一键数据做占位(v4 的 keepPreviousData 选项已移除)
refetchOnWindowFocus: true, // 窗口聚焦重取(stale 时)
initialData: undefined, // 预置缓存数据;新鲜程度受 staleTime 与 initialDataUpdatedAt 影响
meta: {}, // 透传给缓存与全局回调的元信息
})| 字段 | 含义 |
|---|---|
data |
查询数据,类型由 queryFn 推断;isPending 为 true 时收窄为 undefined(v5 严格类型收窄) |
error |
出错对象(默认为 Error) |
status |
'pending' | 'error' | 'success' |
fetchStatus |
'idle' | 'fetching' | 'paused' |
isPending / isSuccess / isError |
status 的布尔化 |
isFetching |
任何请求进行中(含后台重取) |
isLoading |
isPending && isFetching(首次加载) |
dataUpdatedAt |
数据最后成功时间戳 |
failureCount / failureReason |
重试进度 |
isPaused |
网络离线暂停中 |
refetch |
手动重新获取 |
isPlaceholderData |
当前展示的是占位数据 |
- 键须可序列化且能唯一表达结果;函数或特殊对象可能碰撞或丢失信息,使用数组、普通对象和可序列化值
select结果引用不稳定时组件会高频重渲染,复杂派生用useMemo包在 select 外enabled: false且没有缓存时可能是 pending/idle;已有缓存仍可显示成功数据,不能把禁用查询都视作正在请求- v5 中
isPending期间data是undefined;const 解构后的判别联合控制流分析自 TS 4.6 支持——解构后同时排除isPending与isError即把data收窄为TData(if (isPending || isError) return ...、顺序两条 if 或if (!isPending && !isError)均可,对象访问同样适用);只排除isPending不收窄(error 分支同为isPending: false),需再排除isError或判isSuccess。需要"保证非空"的渲染时改用useSuspenseQuery
封装命令式操作(例如 POST/PUT/DELETE),由 mutate 或 mutateAsync 触发;结果和状态记录在 MutationCache,不会自动同步 QueryCache 中的查询结果。
const mutation = useMutation({
mutationFn: (vars: { id: number; title: string }) => api.update(vars),
mutationKey: ['update-todo'], // 可选,配合全局回调
onMutate: async (vars) => ({ snapshot: /* 缓存快照 */ null }),
onSuccess: (data, vars, context) => {},
onError: (error, vars, context) => {},
onSettled: (data, error, vars, context) => {},
retry: 0, // 写操作一般不重试
})| 字段 | 含义 |
|---|---|
mutate(vars) |
触发(错误不会抛出,交 onError) |
mutateAsync(vars) |
触发并返回 Promise,可 await/try-catch |
isPending / isError / isSuccess |
执行状态 |
data / error / variables |
最近一次的产物与入参 |
reset() |
清空回初始状态 |
- 传给 mutate 的单次调用回调在组件卸载后可能不执行;与 useMutation 选项中定义的生命周期回调区分。mutateAsync 的拒绝仍需要调用者处理
- 同一组件多次快速
mutate只保留最后一次结果的状态 onSuccess里手动setQueryData同步多个列表是维护噩梦,优先invalidateQueries- mutation 回调与 MutationCache 全局回调都能拿到 client——v5 两类回调的末位参数注入
context(MutationFunctionContext),context.client即当前 QueryClient(MutationCache 的onSuccess/onError为第 5 参、onSettled为第 6 参;mutation实例上没有client属性)——组件内也常先const queryClient = useQueryClient()(见上文示例),或用useMutationState做全局观测
通过 useQueryClient() 获取实例。
| 方法 | 作用 | 典型用法 |
|---|---|---|
invalidateQueries({ queryKey, exact?, refetchType? }) |
标记失效(可选重取) | 写成功后刷新列表 |
refetchQueries({ queryKey }) |
无视 stale 直接重取 | 强制刷新 |
fetchQuery({ queryKey, queryFn }) |
取数并返回 Promise(进缓存) | 事件回调中预取 |
prefetchQuery({ queryKey, queryFn }) |
fetchQuery 的静默版 | 路由预加载 |
getQueryData(key) |
同步读缓存 | 乐观更新读快照 |
setQueryData(key, updater) |
同步写缓存(updater 返回 undefined 是 no-op,不写入也不清空;移除条目用 removeQueries) |
乐观更新写预测值 |
getQueryState(key) |
读状态元信息 | 判断是否正在取数 |
cancelQueries({ queryKey }) |
取消进行中的请求 | 乐观更新前防覆盖 |
removeQueries({ queryKey }) |
物理删除缓存条目 | 登出清数据 |
resetQueries({ queryKey }) |
重置为初始态 | 重试一切 |
isFetching({ queryKey }) |
是否有请求进行中 | 全局 loading 条 |
clear() |
清空整个缓存 | 退出登录 |
dehydrate(client) / hydrate(client, dehydrated) |
缓存序列化/还原 | SSR 数据传递 |
// 前缀匹配默认是"包含式"的
queryClient.invalidateQueries({ queryKey: ['todos'] }) // 匹配 ['todos', ...] 全部
queryClient.invalidateQueries({ queryKey: ['todos', 1], exact: true }) // 仅精确条目invalidateQueries默认只对挂载中的查询立即重取(refetchType: 'active')setQueryData的 updater 返回undefined是 no-op;它不会创建或清空缓存条目。删除条目请用removeQueriesinitialData会重置 staleTime 计时;只想"占位不标鲜"用placeholderData
QueryClient 的完整失效与重取行为依赖 TanStack Query 运行时。下面只把本页“键按数组前缀组织”的纯逻辑写成完整程序,用来防止文档中的键工厂示例发生排序或层级漂移。
const todoKeys = {
all: ['todos'],
lists: () => [...todoKeys.all, 'list'],
list: (filters) => [...todoKeys.lists(), filters],
detail: (id) => [...todoKeys.all, 'detail', id],
};
const actual = [todoKeys.all, todoKeys.lists(), todoKeys.list({ page: 2 }), todoKeys.detail(3)];
const expected = [['todos'], ['todos', 'list'], ['todos', 'list', { page: 2 }], ['todos', 'detail', 3]];
if (JSON.stringify(actual) !== JSON.stringify(expected)) throw new Error(JSON.stringify(actual));
console.log('Query key factory contracts passed');- 📄 缓存键、staleTime 与失效策略 - 缓存设计规则
- 📄 Query 基础 - 教程入口
- 📄 乐观更新与失效 - 组合应用
- 📄 语法速查 - 一行式签名