TanStack Query 数据请求与缓存:服务端状态的一体化方案

引言

「把 API 数据放进 useState + useEffect」是 React 新手的第一课,也是无数生产事故的温床:竞态条件、重复请求、缓存失效、loading/error 状态散落各处……TanStack Query(曾用名 React Query)正是为此而生——它把「服务端状态」从组件中抽离,用一套完整的状态机 + 缓存系统统一管理。本文将从核心概念、缓存生命周期、派生查询、失效策略到性能优化,系统拆解这套方案。

一、核心概念:Query、Mutation 与 QueryClient

TanStack Query 的三个基本单元:Query(读操作,有缓存)、Mutation(写操作,无缓存)、QueryClient(容器,管理缓存与默认配置)。

三者关系

flowchart TB
  classDef q1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
  classDef q2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
  classDef q3 fill:#e0f7fa,stroke:#00838f,color:#004d40

  A[QueryClientProvider] --> B[QueryClient 实例]
  B --> C[Query 缓存 Map]
  B --> D[Mutation 队列]
  B --> E[默认配置 defaultOptions]

  C --> F[useQuery 组件订阅]
  C --> G[prefetchQuery 预取]
  D --> H[useMutation 提交]
  H --> I[onSuccess 使缓存失效]

  click C "https://tanstack.com/query/latest/docs/framework/react/reference/useQuery" "useQuery 文档"
  class A q1
  class B q1
  class C q2
  class D q2
  class E q2
  class F q3
  class G q3
  class H q3
  class I q3

QueryKey:缓存的寻址方式

每个 Query 由一个 QueryKey(数组)唯一标识。key 是「缓存地址」,不是「请求标识」——同名不同参数(如 ['posts', page])会产生不同缓存条目。

flowchart LR
  classDef k1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
  classDef k2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
  classDef k3 fill:#e0f7fa,stroke:#00838f,color:#004d40

  A["queryKey: ['posts', { page: 1 }]"] --> B[缓存键序列化]
  B --> C[缓存条目 A]
  D["queryKey: ['posts', { page: 2 }]"] --> E[缓存条目 B]
  F["queryKey: ['user', 42]"] --> G[缓存条目 C]

  H["invalidateQueries 按前缀匹配"] --> I[前缀匹配 全部失效]

  click H "https://tanstack.com/query/latest/docs/framework/react/guides/invalidations" "失效策略文档"
  class A k1
  class B k1
  class C k2
  class D k1
  class E k2
  class F k1
  class G k2
  class H k3
  class I k3

实践要点:QueryKey 是分层组织的最佳方式——['posts', 'detail', slug]['user', 'profile']。失效时用前缀匹配 ['posts'] 即可批量失效列表与详情。

二、查询生命周期与状态机

TanStack Query 的核心机制是陈旧数据(Stale-While-Revalidate):缓存的数据可以立即展示(即使已过期),同时后台静默刷新。这让界面永远「有内容」,而不是转圈。

查询状态机

stateDiagram-v2
  direction LR
  [*] --> Pending: 首次挂载
  Pending --> Success: 请求成功
  Pending --> Error: 请求失败
  Success --> Refetching: 手动/自动重取
  Refetching --> Success: 成功
  Refetching --> Error: 失败
  Success --> Error: 后台刷新失败
  Error --> Pending: 重试触发
  Error --> Success: 重试成功

  note right of Pending
    无缓存数据时显示 loading
    有缓存时直接展示缓存
  end note

缓存的四种状态

状态 说明 默认行为
fresh 新鲜数据,无需重取 staleTime 内直接使用
stale 过期数据 展示 + 后台重取
fetching 请求进行中 数据展示同时刷新
paused 请求被暂停 网络恢复自动重试

完整生命周期时序

sequenceDiagram
  participant C as 组件
  participant Q as Query
  participant S as 服务端

  C->>Q: useQuery 挂载
  Q->>Q: 查询缓存
  alt 有缓存且未过期
    Q-->>C: 立即返回缓存
  else 有缓存但过期
    Q-->>C: 返回缓存(stale)
    Q->>S: 后台重新请求
    S-->>Q: 新数据
    Q-->>C: 更新并通知
  else 无缓存
    Q->>S: 发起请求
    S-->>Q: 数据
    Q-->>C: 返回并缓存
  end
  C->>Q: 组件卸载
  Q->>Q: gcTime 后回收缓存

三、失效与更新策略

服务端状态的难点在于何时失效。TanStack Query 提供四层失效手段,从粗到细:

失效手段对比

flowchart TB
  classDef i1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
  classDef i2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
  classDef i3 fill:#e0f7fa,stroke:#00838f,color:#004d40

  A[失效时机] --> B[invalidateQueries<br/>按 key 前缀批量失效]
  A --> C[setQueryData<br/>直接写入缓存]
  A --> D[fetchQuery<br/>强制重取]
  A --> E[refetchInterval<br/>轮询刷新]

  B --> F[写操作成功后调用]
  C --> G[乐观更新 回滚机制]
  D --> H[关键数据强制刷新]
  E --> I[实时数据 如在线状态]

  click B "https://tanstack.com/query/latest/docs/framework/react/guides/invalidations" "失效文档"
  class A i1
  class B i1
  class C i2
  class D i2
  class E i2
  class F i3
  class G i3
  class H i3
  class I i3

乐观更新的完整流程

乐观更新(Optimistic Update)让 UI 在服务端确认前就展示新状态,再根据结果提交或回滚:

sequenceDiagram
  participant U as 用户
  participant C as 组件
  participant Q as Query 缓存
  participant S as API

  U->>C: 提交变更
  C->>Q: 立即写入新值(乐观)
  Q-->>C: UI 即时更新
  C->>S: 发起真实请求
  alt 成功
    S-->>C: 200
    C->>Q: 用服务端响应覆盖
  else 失败
    S-->>C: 错误
    C->>Q: 回滚到旧值
    C->>C: 显示错误提示
  end

实现建议useMutationonMutate 中执行乐观写入并返回「回滚函数」,onError 中调用回滚,onSettled 中统一 invalidateQueries 对齐真实数据。

四、派生查询与组合模式

真实应用很少是单一查询——列表 + 筛选 + 详情 + 关联推荐,查询之间存在派生与依赖关系。

组合查询模式

flowchart TB
  classDef c1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
  classDef c2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
  classDef c3 fill:#e0f7fa,stroke:#00838f,color:#004d40

  A[帖子详情页] --> B[主查询 postDetail]
  B --> C{依赖主查询数据}
  C -->|评论列表| D[useQuery enabled 条件]
  C -->|作者信息| E[useQuery 参数来自主数据]
  C -->|相关推荐| F[独立并行查询]
  D --> G[评论数据]
  E --> H[作者数据]
  F --> I[推荐数据]

  click D "https://tanstack.com/query/latest/docs/framework/react/guides/dependent-queries" "依赖查询文档"
  class A c1
  class B c1
  class C c1
  class D c2
  class E c2
  class F c2
  class G c3
  class H c3
  class I c3

常用组合查询 API

API 用途 特点
useQuery 单一数据源 最常用
useQueries 动态并行查询 数量运行时确定
useInfiniteQuery 分页/无限滚动 游标推进
enabled 条件查询 依赖前置查询
placeholderData 占位数据 保持上次数据平滑过渡
initialData 初始数据 服务端注入兜底

五、性能优化实践

请求去重与窗口聚焦

TanStack Query 内置请求去重:同一 QueryKey 并发挂载多个组件,只会发起一次请求。此外,refetchOnWindowFocus 默认开启——用户切回标签页时自动刷新数据,适合「价格、库存、消息数」等易变场景;若数据变化不敏感,可关闭以省流量。

性能优化清单

flowchart TD
  classDef p1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
  classDef p2 fill:#fff3e0,stroke:#f57c00,color:#e65100
  classDef p3 fill:#e8f5e9,stroke:#388e3c,color:#1b5e20

  A[Query 性能优化] --> B[减少请求]
  B --> B1[staleTime 拉长]
  B --> B2[请求去重 共享 QueryKey]
  B --> B3[预取 prefetchQuery]
  A --> C[减少渲染]
  C --> C1[select 只取所需字段]
  C --> C2[结构共享避免新引用]
  C --> C3[缓存数据引用稳定]
  A --> D[减少流量]
  D --> D1[按需字段服务端裁剪]
  D --> D2[条件查询 enabled]
  D --> D3[关闭窗口聚焦刷新]

  click C1 "https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-react-query-4#select" "select 优化文档"
  class A p1
  class B p1
  class B1 p3
  class B2 p3
  class B3 p3
  class C p1
  class C1 p3
  class C2 p3
  class C3 p3
  class D p1
  class D1 p3
  class D2 p3
  class D3 p3

常见陷阱

陷阱 表现 对策
缓存过期不失效 数据一直旧 写后 invalidate
QueryKey 不一致 缓存永远不命中 统一 queryKey 工厂
忽略 error 界面一直转圈 处理 isError 分支
循环 refetch 无限请求 检查依赖数组
大对象 select 重渲染频繁 返回原始类型/浅层对象

六、SSR 集成:预取与脱水

TanStack Query 与 SSR 配合的标准姿势:服务端 prefetchQuery 预热缓存,dehydrate 序列化注入 HTML,客户端 hydrate 恢复缓存。这与我们上一篇文章《React SSR 与流式渲染》中的「数据对齐」章节完全吻合。

flowchart LR
  classDef s1 fill:#e3f2fd,stroke:#1976d2,color:#0d47a1
  classDef s2 fill:#f3e5f5,stroke:#8e24aa,color:#4a148c
  classDef s3 fill:#e0f7fa,stroke:#00838f,color:#004d40

  A[服务端请求] --> B[prefetchQuery 预取]
  B --> C[渲染组件 HTML]
  C --> D[dehydrate 序列化缓存]
  D --> E[注入 window.__REACT_QUERY_STATE__]
  E --> F[客户端加载]
  F --> G[hydrate 恢复缓存]
  G --> H[组件直接读缓存 不重复请求]

  click B "https://tanstack.com/query/latest/docs/framework/react/guides/ssr" "SSR 集成文档"
  class A s1
  class B s1
  class C s2
  class D s2
  class E s3
  class F s1
  class G s2
  class H s3

七、总结

TanStack Query 把「服务端状态」变成了一门有纪律的学科:

  1. 状态机驱动:pending/success/error/stale 四态清晰,UI 分支完备;
  2. 缓存为核心:QueryKey 寻址、SWR 策略、gcTime 回收;
  3. 失效即正确:写后失效、乐观更新、回滚机制完备;
  4. 派生与组合:依赖查询、无限滚动、并行查询覆盖全部场景;
  5. SSR 友好:预取脱水注水,双端数据天然一致。

一句话总结:把服务端数据交给 TanStack Query,把精力还给业务逻辑。下一篇我们将深入 React 性能优化专题,从渲染层面全面提升应用流畅度。