TanStack Query 缓存失效模式
TanStack Query 的缓存同步操作:cancelQueries、setQueryData、removeQueries、invalidateQueries,以及如何根据 authoritative evidence、冲突与 projection 影响范围选择策略。
[!info] related notes
- 所属 MOC: TanStack Query 知识地图
- 前置: TanStack Query 服务端状态
- 模式: queryKey Factory 模式
- 实践: TanStack Query 与 SSE 流式数据集成
- 迁移: 从 useState 迁移到 TanStack Query
- 关联: Mutation 完整生命周期
TanStack Query 缓存失效模式
核心问题
TanStack Query 的缓存操作不是单纯的“性能 API”。真正的问题是:
服务器事实变化后,客户端掌握了多少可信的新信息,应该直接修正 projection,还是承认自己不知道完整新状态并重新同步?
Query Cache 是 server state 的客户端 projection,不是 durable truth。因此 setQueryData 与 invalidateQueries 的选择首先是证据与 authority 问题,其次才是延迟/请求数问题。
四个 API 的分工
cancelQueries:取消客户端 Query 的在途 fetch
await queryClient.cancelQueries({ queryKey });
常用于乐观更新或准备手工写 cache 前,降低旧响应后来覆盖新 projection 的风险。
它取消的是 Query 的请求生命周期,不等于撤销已经发生的服务端业务副作用。
setQueryData:客户端明确写入 projection
queryClient.setQueryData(['user', user.id], user);
适合客户端已经有足够证据知道新 cache 应该长什么样的情况,例如 Mutation 返回了完整、权威、更新后的 resource。
核心条件不是“字段看起来很好猜”,而是:
Client has sufficient authoritative evidence
→ can construct the cache entry correctly
removeQueries:删除 cache entry
queryClient.removeQueries({
queryKey: ['conversation', deletedId],
});
适合资源已经删除、不应继续保留旧详情缓存,或者明确要释放无用缓存。
删除缓存不是重新同步;如果未来再次需要数据,Query 会重新 fetch。
invalidateQueries:撤销 freshness 信任并重新同步
queryClient.invalidateQueries({ queryKey });
它表达的是:
我不再相信这份 projection 足够新。
匹配 Query 会被标记 stale;活跃 Query 通常会按 TanStack Query 的 refetch policy 重新获取,非活跃 Query 可在之后使用时再同步。
最重要的决策:setQueryData vs invalidateQueries
Mutation 返回完整、权威的新 Resource
→ setQueryData 可以是正确且低延迟的
Mutation 只返回 { success: true, revision: 43 }
且一次操作可能改变多个字段/聚合
→ invalidate/refetch 更稳
所以不要把规则背成:
setQueryData = 快
invalidate = 慢但安全
更准确是:
setQueryData
= 我有证据知道新的 projection
invalidateQueries
= 我知道旧 projection 不再可信,但不知道完整新事实
409 stale revision 为什么尤其应该 refetch
典型 OCC 场景:
Client cache revision = 42
Client sends expected_revision = 42
Server current revision = 43
→ 409 Conflict
409 不只是“请求失败”,还提供了一个重要事实:
客户端当前依据的 server projection 已经过期。
因此:
409 stale conflict
→ reject local stale write
→ invalidate affected server-state queries
→ fetch current authoritative revision
→ let UI converge
这和普通 400 invalid input 不一样。一个明确的输入校验失败通常没有证据说明服务器事实已经变化,因此不需要无脑 invalidate 整个领域缓存。
BodySense 当前模式
BodyState command 当前是:
mutation success
→ invalidate workspace
409 stale revision
→ invalidate workspace
ordinary unrelated error
→ no blind workspace invalidation
对应的意图非常清晰:
success
→ server truth changed
409
→ client projection proved stale
other error
→ no evidence that workspace truth changed
当前代码锚点:
apps/web/src/features/workspace/hooks/useBodyStateCommand.ts
apps/web/src/features/workspace/hooks/useWorkspaceInvalidation.ts
其中 workspace 以 workspaceKeys.all 做前缀 invalidation,让所有受 BodyState command 影响的 workspace projection 一起重新同步。
Invalidation key 应该多宽
原则:
As broad as necessary, as narrow as justified.
太窄:
只 invalidate detail
但 list/summary 也受影响
→ projection drift
太宽:
修改一个 title
→ invalidate 整个应用所有 queries
→ 不必要 refetch / 抖动
Query-key hierarchy 的价值之一,就是能把“受影响 projection 的范围”表达为前缀。
const workspaceKeys = {
all: ['workspace'] as const,
health: () => [...workspaceKeys.all, 'health'] as const,
};
如果一个 mutation 的领域影响跨多个 workspace query,invalidate workspaceKeys.all 是有意的 broad invalidation;如果只改变一个独立 detail,则应更精确。
乐观更新仍然需要 reconciliation
经典流程:
cancel in-flight query
→ snapshot previous cache
→ set optimistic projection
→ mutation
├─ failure → rollback
└─ success → use authoritative response or invalidate
乐观值只是预测,不是服务器事实。尤其在带 revision/OCC、server-computed fields 或复杂聚合更新时,不要因为 UI 已经看起来正确就跳过最终 reconciliation。
cancelQueries 不是万能前置
“任何 setQueryData 前都必须 cancel”也过度绝对。
真正需要分析的是:是否存在一个 in-flight fetch 会在之后提交旧结果并覆盖你刚写的 projection;TanStack Query 的具体取消/版本行为也取决于 queryFn 是否消费 AbortSignal 以及当前生命周期。乐观更新时 cancelQueries 是非常常见的防竞态步骤,但不应被背成脱离场景的仪式。
常见模式
| 场景 | 常见策略 | 原因 |
|---|---|---|
| Mutation 返回完整 authoritative resource | setQueryData | 已知新 projection |
| Mutation 只返回成功/revision | invalidateQueries | 不知道完整后果 |
| OCC 返回 409 stale revision | invalidateQueries | 已证明本地 projection stale |
| 明确 400 validation error | 通常不 invalidate | 没证据说明 server truth 变化 |
| 删除资源 | 更新 list + removeQueries(detail) | 已删除详情不应留存 |
| 乐观更新 | cancel + snapshot + set + rollback/reconcile | 先体验,后确认 |
| 流式 runtime 完成 | reconcile/invalidate durable read model | live projection 回归 server truth |
常见错误
把 setQueryData 当成“改服务器”
setQueryData
→ only changes client cache
它不会替你执行业务 Mutation,也不赋予客户端 authority。
Mutation 成功后什么都不做
如果当前 cache 没被 mutation response 更新,也没有 invalidate,UI 可以长期停留在旧 server projection。
409 后继续相信旧 cache
这相当于已经得到“你是 stale 的”证据,却拒绝重新同步。
所有错误都 invalidateQueries({ queryKey: root })
会制造不必要网络负载,也掩盖不同错误语义。
只更新 list 或只更新 detail
TanStack Query 不是 normalized cache。同一实体可能存在多个 Query projection;要明确所有受影响 entry,或者选择合适的前缀 invalidation。
最短记忆
setQueryData
= 我知道新的客户端 projection 应该是什么
invalidateQueries
= 我知道旧 projection 不可信,但完整新事实应该向服务器重新问
409 stale
= 本地 projection 过期的证据
400 invalid input
= 请求不合法,不自动证明 server state 变化