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
两种路线:
- Invalidate 驱动(推荐默认):
- mutation 成功 → invalidate 相关 queries → 由 query 统一刷新
- 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 是缓存真相
典型模式(强烈推荐):
- 把筛选/分页等条件放进 URL(source of truth)
- 从 URL 派生 queryKey
- 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 缓存双重管理?
关联阅读
- 状态分层:
./01-state-layering-ui-server-url-global.md - Suspense 与加载态边界:
../09-react-18-in-practice/04-suspense-loading-boundary-design.md - 路由章节(URL state 的承载层):
../10-routing-and-navigation/README.md