← [ BACK_TO_NOTES ]

TanStack Query 缓存失效模式

TanStack Query 的缓存同步操作:cancelQueries、setQueryData、removeQueries、invalidateQueries,以及如何根据 authoritative evidence、冲突与 projection 影响范围选择策略。

#howto #growing #frontend #react

[!info] related notes

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 resourcesetQueryData已知新 projection
Mutation 只返回成功/revisioninvalidateQueries不知道完整后果
OCC 返回 409 stale revisioninvalidateQueries已证明本地 projection stale
明确 400 validation error通常不 invalidate没证据说明 server truth 变化
删除资源更新 list + removeQueries(detail)已删除详情不应留存
乐观更新cancel + snapshot + set + rollback/reconcile先体验,后确认
流式 runtime 完成reconcile/invalidate durable read modellive 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 变化
创建于 2026/7/2 更新于 2026/9/16
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本