Chapter 06

查询失效与手动更新

用 queryClient 主动操控缓存——失效、直写、重取、预取,做到数据同步的精细控制。

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 方法

6.8 小结与下一步