Notes

TanStack Query:缓存模型与失效策略(Caching & Invalidation)

一句话结论:TanStack Query(React Query)把 Server State 当成“带缓存一致性的查询结果”来管理——你不再手写 loading/error/缓存/去重,而是通过 queryKey(身份) + stale/gc(生命周期) + invalidate/refetch(失效与刷新) 建立一套可控的数据一致性系统;真正的工程关键是:queryKey 设计、失效粒度、预取策略、以及与 URL State / 路由 / Next 缓存的边界

0. 前置:它管理的是 Server State,不是所有状态

在上一篇“状态分层”里我们强调:

  • URL State(筛选/分页/排序)应以 URL 为真相
  • Server State(列表/详情等)应以 Query cache 为真相

TanStack Query 适合管理:

  • “来自服务端 + 需要缓存一致性”的数据

不适合用它承载:

  • UI state(弹窗开关)
  • 纯全局配置但不依赖请求一致性的状态(例如主题)

1. Query 的核心对象:queryKey 与 queryFn

1.1 queryKey:缓存的“身份”

你可以把 queryKey 当成缓存系统的主键(cache key)。

  • key 相同 → 命中同一份缓存
  • key 不同 → 视为不同查询(不同缓存槽)

工程建议:queryKey 必须只包含“决定结果”的输入”。

例如:

  • ['products', { keyword, page, sort, filters }]
  • ['products', { keyword, page }, { uiOpen: true }](把 UI state 混进来会导致缓存碎片化)

1.2 queryFn:如何获取

Query 负责“缓存与一致性”,而 queryFn 负责“请求”。

建议收敛在统一的 API 层(带鉴权、traceId、错误规范),避免 queryFn 到处散落。

2. 缓存生命周期:staleTime / gcTime(原 cacheTime)

2.1 stale:是否“新鲜”

  • staleTime:数据在多久内视为“新鲜”(不会因为重新挂载就自动 refetch)

理解方式:

  • staleTime 越大:更少自动刷新,更依赖你主动 invalidate(适合稳定数据、降低请求)
  • staleTime 越小:更频繁自动刷新(适合强一致需求,但要控制请求风暴)

2.2 gc:缓存何时被垃圾回收

  • gcTime:当某个 query 没有观察者(没有组件在用)后,多久清掉缓存

理解方式:

  • gcTime 影响“返回上一页是否秒开”
  • 对移动端、低内存设备要更谨慎

版本提示:新版本把 cacheTime 更名为 gcTime,语义更准确。

3. 失效与刷新:invalidateQueries vs refetchQueries

3.1 invalidateQueries:让缓存变“脏”

  • 将匹配的 query 标记为 stale
  • 下一次合适的时机(组件在用、或配置允许)会触发 refetch

适合场景:

  • 你完成了 mutation(新增/编辑/删除),需要让相关列表/详情在后续刷新

3.2 refetchQueries:立即重新拉取

  • 直接发起请求刷新

适合场景:

  • 用户显式点击“刷新”
  • 你明确知道必须立刻同步 UI

3.3 工程策略:通常 mutation 后用 invalidate 而不是手动 setQueryData

两种路线:

  1. Invalidate 驱动(推荐默认)
  • mutation 成功 → invalidate 相关 queries → 由 query 统一刷新
  1. Cache Update 驱动(进阶)
  • mutation 成功 → setQueryData 直接更新缓存(速度快)
  • 再在后台 invalidate 做一致性兜底

经验:架构上应先把 invalidate 体系建立好,再逐步引入 setQueryData/乐观更新。

4. 并发去重与状态机:你不再手写“请求锁”

TanStack Query 默认能处理很多工程脏活:

  • 同一 queryKey 多组件订阅:共享一份请求与结果
  • loading/error/success 状态统一
  • retry/backoff、cancel、focus/refetch 等策略

你应该把精力放在:

  • key 设计
  • 失效粒度
  • 与路由/URL 的协作

5. 与 URL State 的协作:URL 是条件真相,QueryKey 是缓存真相

典型模式(强烈推荐):

  1. 把筛选/分页等条件放进 URL(source of truth)
  2. 从 URL 派生 queryKey
  3. Query 使用 queryKey 拉数据并缓存

这样保证:

  • 分享链接可还原
  • back/forward 可回放
  • 缓存命中稳定(同一 URL → 同一 key)

6. 预取(prefetch):把“等待”前移,提升导航体验

6.1 SPA(TanStack Router + Vite)

常见做法:

  • 路由进入前:prefetch list
  • hover 链接:prefetch detail

关键点:

  • prefetch 使用同一套 queryKey(否则预取无法命中)
  • 预取要有预算(别把用户带宽打满)

6.2 Next.js

在 Next(尤其 App Router)里你可能会同时面对两套缓存:

  • Next 的 fetch 缓存/重验证(偏框架级)
  • TanStack Query 的 client cache(偏交互级)

架构建议:

  • Server Components 获取的数据:优先走 Next 缓存体系(并建立 revalidate 策略)
  • Client Components 交互数据(筛选/无限滚动/局部刷新):用 TanStack Query

不要让同一数据域同时由两套缓存系统“各自为政”。

7. 失效粒度:写出“既正确又不浪费”的 invalidate

7.1 最小原则:只 invalidates 受影响的域

  • 新增一条评论 → invalidate ['comments', postId]
  • 修改用户资料 → invalidate ['user', userId],以及可能依赖它的 ['me']

7.2 常见误区

  • 直接 invalidateQueries() 全清:简单但会造成请求风暴与性能抖动
  • key 设计不稳定(对象引用不稳定):导致命中率低

8. 企业级建议:统一 QueryClient 配置 + 统一上报

8.1 QueryClient 统一配置(示意)

import { QueryClient } from '@tanstack/react-query'

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 30_000,
      gcTime: 5 * 60_000,
      retry: 1,
      refetchOnWindowFocus: false,
    },
  },
})

8.2 可观测:请求错误与重试要可追踪

建议在请求层做:

  • traceId
  • 统一错误码
  • 与 Sentry/自研上报打通(尤其是 query error 的聚合与告警)

9. Checklist

  • queryKey 是否只包含“决定结果”的输入?是否稳定、可序列化?
  • URL state 是否作为查询条件的 source of truth?
  • mutation 后是否有清晰的 invalidate 规则(粒度合适)?
  • staleTime/gcTime 是否符合业务一致性与体验预期?
  • 是否避免同一数据域被 Next 缓存与 Query 缓存双重管理?

关联阅读

cd ..