6.1 缓存不是黑盒
前面章节里缓存大多自动运转,但真实应用常需主动干预:改完数据让列表刷新、鼠标悬停就预加载详情、WebSocket 推送后直接写入缓存。这些都靠 queryClient 的命令式 API 完成。
6.2 核心概念
- invalidateQueries
- 把匹配的查询标记为 stale,并对当前 active 的查询立即触发后台重取。是"数据变了,重新拿"的首选。支持前缀模糊匹配一次失效一批。
- setQueryData
- 直接把新数据写进缓存,不发请求。用于乐观更新、把 mutation 响应写回、或 WebSocket 推送更新。接收新值或
(old) => new更新函数。 - refetch
- useQuery 返回的方法,强制重新执行当前这一个查询,无视 staleTime。适合"手动刷新"按钮。
- prefetchQuery
- 提前把数据拉进缓存,用户真正需要时秒开。常用于路由跳转前、列表项 hover 时预加载详情。
- getQueryData / ensureQueryData
- getQueryData 同步读缓存(可能 undefined);ensureQueryData 若无缓存则拉取后返回,保证有值——loader 里常用。
- 查询过滤器 (Query Filters)
- invalidateQueries 等方法接收的匹配条件:
queryKey前缀、exact精确匹配、type: 'active'、predicate自定义函数——决定操作作用于哪些查询。 - cancelQueries
- 取消进行中的请求,乐观更新前调用可避免旧响应覆盖新 UI。
6.3 invalidateQueries:模糊 vs 精确
const qc = useQueryClient();
// 前缀匹配:失效所有以 ["todos"] 开头的查询
qc.invalidateQueries({ queryKey: ["todos"] });
// → 命中 ["todos"], ["todos","list"], ["todos","detail",5] ...
// 精确匹配:只失效这一个
qc.invalidateQueries({ queryKey: ["todos", "list"], exact: true });
// 自定义谓词:只失效已完成的列表
qc.invalidateQueries({
predicate: (q) => q.queryKey[0] === "todos" && q.state.data?.done,
});
层级 key 的威力
正因为 invalidate 默认前缀匹配,第 2 章强调的"层级化 queryKey"才如此重要——一次 invalidateQueries({ queryKey: ["todos"] }) 就能同步所有 todo 相关缓存,不必逐个列举。
6.4 setQueryData:直接改缓存
// 把 mutation 的响应直接写回,省一次重取
const mutation = useMutation({
mutationFn: updateTodo,
onSuccess: (updated) => {
qc.setQueryData<Todo>(["todos", "detail", updated.id], updated);
qc.setQueryData<Todo[]>(["todos", "list"], (old) =>
old?.map(t => (t.id === updated.id ? updated : t))
);
},
});
// WebSocket 推送新消息 → 直接追加,无需请求
socket.on("message", (msg) => {
qc.setQueryData<Msg[]>(["messages"], (old) => [...(old ?? []), msg]);
});
6.5 invalidate 还是 setQueryData?
| 需求 | 推荐 | 原因 |
|---|---|---|
| 改完想拿服务器权威数据 | invalidateQueries | 重取保证一致,最稳 |
| 响应已含完整新数据 | setQueryData | 省一次网络往返 |
| 实时推送(WS/SSE) | setQueryData | 推什么写什么 |
| 不确定服务器会否附加计算字段 | invalidateQueries | 避免本地数据不全 |
6.6 预取 prefetchQuery
// 鼠标悬停列表项时预加载详情,点进去秒开
function TodoItem({ id }: { id: number }) {
const qc = useQueryClient();
const prefetch = () =>
qc.prefetchQuery({
queryKey: ["todos", "detail", id],
queryFn: () => fetchTodo(id),
staleTime: 10_000, // 10s 内不重复预取
});
return <li onMouseEnter={prefetch}>…</li>;
}
prefetch 不订阅
prefetchQuery 只是把数据放进缓存,不返回响应式的 data。真正渲染那份数据还是靠组件里的 useQuery——它会命中已预取的缓存,直接 success。
6.7 其他常用 queryClient 方法
qc.removeQueries({ queryKey }):彻底删除缓存(如登出时清空)。qc.resetQueries({ queryKey }):重置到初始状态并重取。qc.getQueriesData({ queryKey }):批量读取匹配的多份缓存。qc.fetchQuery({ queryKey, queryFn }):命令式拉取并返回 Promise(loader 用)。qc.setQueryDefaults(key, opts):给某类 key 设默认配置。
6.8 小结与下一步
- invalidateQueries 前缀匹配批量失效,是数据同步首选。
- setQueryData 直写缓存,适合乐观更新、写回响应、实时推送。
- prefetchQuery 提前拉数据进缓存,实现 hover/路由预加载。
- queryClient 还有 remove/reset/fetch 等命令式方法应对各种场景。
- 下一章聚焦 请求状态与错误处理——retry、错误边界、Suspense。