服务端状态与数据获取库技术选型对比:React Query vs SWR vs Apollo Client vs RTK Query vs React Router
【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query
本文以本仓库(TanStack Query 一体化仓库)内 docs/framework/react/comparison.md 中的官方对比文档为核心骨架,对 React 生态中五个主流服务端状态方案进行逐维度横评,并深入 React Query 在本仓库中的源码实现(query-core 与 react-query 适配层),解释"表格中的每一项 ✅ 究竟靠什么代码实现"。读完本文,你将能读懂一张全量能力对比表背后的架构差异,理解 React Query 的确定性缓存序列化、结构共享、渲染追踪等设计如何落地,从而在真实项目中做出有依据的技术选型。
对比的定位与符号约定
该对比文档的初衷是"尽可能准确、尽可能无偏":表格结论面向所有被比较的库保持同一套标准,社区贡献者可通过页面底部的编辑入口在附上证据后修正信息。比较的五个对象分别是:
| 库 | 定位 | 项目语境 |
|---|---|---|
| React Query | TanStack Query 的 React 实现 | 本文所在仓库 packages/react-query |
| SWR | Vercel 出品的轻量数据请求库 | 外部独立项目(不在本仓库内) |
| Apollo Client | GraphQL 官方阵营客户端 | 外部独立项目(不在本仓库内) |
| RTK Query | Redux Toolkit 内置数据获取方案 | 外部独立项目(不在本仓库内) |
| React Router | 路由 + 数据加载(loader) | 外部独立项目(不在本仓库内) |
对比表使用统一的 Feature/Capability Key 图例:
- ✅ 一等公民、内置、开箱即用,无需额外配置或代码;
- 🟡 支持,但需借助非官方的第三方或社区库/贡献实现;
- 🔶 官方支持且有文档,但需要用户额外编写实现代码;
- 🛑 未官方支持或未文档化。
定位、数据源与缓存架构横评
下表对比五者在"平台绑定、数据形态、缓存模型"上的底层差异。缓存架构的分歧是本次对比中最值得先读的部分,因为它决定了上层几乎所有能力的实现成本。
| 维度 | React Query | SWR | Apollo Client | RTK Query | React Router |
|---|---|---|---|---|---|
| 开源仓库(Star 徽章省略) | TanStack/query | vercel/swr | apollographql/apollo-client | reduxjs/redux-toolkit | remix-run/react-router |
| 平台依赖 | React | React | React、GraphQL | Redux | React |
| 官方横向比较页 | 无 | 无 | 无 | 官方提供 | 无 |
| 支持的查询数据源 | Promise、REST、GraphQL | Promise、REST、GraphQL | GraphQL、Any(Reactive Variables) | Promise、REST、GraphQL | Promise、REST、GraphQL |
| 支持框架 | React | React | React 及其他 | 任意 | React |
| 缓存策略 | 分层键 → 值(Hierarchical Key→Value) | 唯一键 → 值 | 规范化 Schema(Normalized) | 唯一键 → 值 | 嵌套路由 → 值 |
| 缓存键策略 | JSON | JSON | GraphQL Query | JSON | 路由路径 |
| 缓存变更检测 | 深比较键(稳定序列化) | 深比较键(稳定序列化) | 深比较键(不稳定序列化) | 键引用相等(===) | 路由变更 |
| 数据变更检测 | 深比较 + 结构共享 | 深比较(经由 stable-hash) | 深比较(不稳定序列化) | 键引用相等(===) | Loader 运行 |
| 数据记忆化 | 完整结构共享 | 身份(===) | 规范化身份 | 身份(===) | 身份(===) |
| 包体积 | min+gzip 实时徽章(原表引用 Bundlephobia,本文不复现外链) | 同左 | 同左 | 同左 | react-router-dom + history 两包合计 |
| API 定义位置 | 组件内、外部配置均可 | 组件内 | GraphQL Schema | 外部配置 | 路由树配置 |
缓存架构差异解读
- Hierarchical Key → Value(React Query):缓存以"查询键 + 查询"为单位组织,查询键支持任意层级嵌套;底层 QueryCache 把每个序列化后的键映射到一条查询状态。
- Unique Key → Value(SWR / RTK Query):同样是键值缓存,但 React Query 的键结构更强调"前缀"式的层级可匹配性(详见下文 Partial Query Matching)。
- Normalized Schema(Apollo Client):数据按实体扁平化存放(entity → record),以此规避高层数据重复,这也是其自动"变更新后重取"能力的基础。
- Nested Route → Value(React Router):缓存生命周期与路由匹配绑定,离开路由即释放(见注解 8)。
值得注意的是"Cache Change Detection"列的差异:React Query 与 SWR 对键本身做稳定序列化 + 深比较,因此相同语义的键不会被重复缓存;Apollo 的序列化相对不稳定;RTK Query 使用键的**引用相等(===)**判断,缓存命中语义因此不同。
查询编排与常规数据能力横评
下表覆盖日常开发最常接触的查询编排能力。可以清晰看到 React Query 在此区间几乎全量 ✅,而 React Router 因"不缓存活动路由之外的数据",在持续性数据能力上大量为 🛑。
| 维度 | React Query | SWR | Apollo Client | RTK Query | React Router |
|---|---|---|---|---|---|
| Queries(基础查询) | ✅ | ✅ | ✅ | ✅ | ✅ |
| Cache Persistence(缓存持久化) | ✅ | ✅ | ✅ | ✅ | 🛑8 |
| Devtools | ✅ | ✅ | ✅ | ✅ | 🛑 |
| Polling/Intervals(轮询/定时刷新) | ✅ | ✅ | ✅ | ✅ | 🛑 |
| Parallel Queries(并行查询) | ✅ | ✅ | ✅ | ✅ | ✅ |
| Dependent Queries(依赖查询) | ✅ | ✅ | ✅ | ✅ | ✅ |
| Paginated Queries(分页查询) | ✅ | ✅ | ✅ | ✅ | ✅ |
| Infinite Queries(无限滚动查询) | ✅ | ✅ | ✅ | ✅ | 🛑 |
| Bi-directional Infinite Queries(双向无限查询) | ✅ | 🔶 | 🔶 | ✅ | 🛑 |
| Infinite Query Refetching(无限查询刷新) | ✅ | ✅ | 🛑 | ✅ | 🛑 |
| Lagged Query Data(新旧数据平滑过渡)1 | ✅ | ✅ | ✅ | ✅ | ✅ |
| Selectors(选择器) | ✅ | 🛑 | ✅ | ✅ | N/A |
| Initial Data(初始数据) | ✅ | ✅ | ✅ | ✅ | ✅ |
| Scroll Recovery(滚动位置恢复) | ✅ | ✅ | ✅ | ✅ | ✅ |
| Cache Manipulation(直接操作缓存) | ✅ | ✅ | ✅ | ✅ | 🛑 |
| Outdated Query Dismissal(丢弃过期结果) | ✅ | ✅ | ✅ | ✅ | ✅ |
| Stale While Revalidate | ✅ | ✅ | ✅ | ✅ | 🛑 |
| Stale Time Configuration(staleTime 可配置) | ✅ | 🛑7 | 🛑 | ✅ | 🛑 |
| Window Focus Refetching(窗口聚焦重取) | ✅ | ✅ | 🛑 | ✅ | 🛑 |
| Network Status Refetching(网络恢复重取) | ✅ | ✅ | ✅ | ✅ | 🛑 |
缓存控制与渲染性能横评
下表聚焦"手动控制、渲染优化、离线与脱水平台能力",这些维度最能拉开各库架构差距。
| 维度 | React Query | SWR | Apollo Client | RTK Query | React Router |
|---|---|---|---|---|---|
| Render Batching & Optimization(渲染批处理与优化)2 | ✅ | ✅ | 🛑 | ✅ | ✅ |
| Auto Garbage Collection(自动垃圾回收) | ✅ | 🛑 | 🛑 | ✅ | N/A |
| Mutation Hooks | ✅ | ✅ | ✅ | ✅ | ✅ |
| Offline Mutation Support(离线变更) | ✅ | 🛑 | 🟡 | 🛑 | 🛑 |
| Prefetching APIs(预取 API) | ✅ | ✅ | ✅ | ✅ | ✅ |
| Query Cancellation(查询取消) | ✅ | 🛑 | 🛑 | 🛑 | ✅ |
| Partial Query Matching(部分查询匹配)3 | ✅ | 🔶 | ✅ | ✅ | N/A |
| Pre-usage Query/Mutation Configuration(用前预配置)4 | ✅ | 🛑 | ✅ | ✅ | ✅ |
| General Cache Dehydration/Rehydration(缓存脱水/注水) | ✅ | 🛑 | ✅ | ✅ | ✅ |
| Offline Caching(离线缓存) | ✅ | 🛑 | ✅ | 🔶 | 🛑 |
| React Suspense | ✅ | ✅ | ✅ | 🛑 | ✅ |
| Abstracted/Agnostic Core(框架无关核心) | ✅ | 🛑 | ✅ | ✅ | 🛑 |
| Automatic Refetch after Mutation(变更后自动重取)5 | 🔶 | 🔶 | ✅ | ✅ | ✅ |
| Normalized Caching(规范化缓存)6 | 🛑 | 🛑 | ✅ | 🛑 | 🛑 |
八条关键差异注解:从"符号"到"源码"
表格之外,对比文档用 8 条注解释放了结论的前提与细节。下面逐条展开,并对 React Query 侧给出本仓库源码级佐证。
注解 1:Lagged Query Data —— 分页 UI 不闪硬加载态的关键
React Query 允许在新查询加载期间继续展示上一条查询的已有数据,避免分页/无限加载场景中出现硬 Loading 态(这也是 Suspense 未来要原生提供的体验)。其他库除非已预取,否则新查询期间会渲染硬加载态。该能力对分页与 Infinite UI 极其重要,源码中由"数据保留策略 + 状态派生"共同支撑(可关注 query-core 中 observer 对data/isPlaceholderData的状态组织)。
注解 2:Render Optimization —— 精确到属性的订阅式渲染
React Query 默认自动追踪组件实际访问了结果对象上的哪些字段,仅在这些字段变化时才触发重渲染。实现上,QueryObserver维护了一个属性追踪集合:
- 定义
#trackedProps = new Set<keyof QueryObserverResult>()(见 packages/query-core/src/queryObserver.ts#L65); - 通过
trackResult返回一个Proxy包装的结果对象,get钩子里调用trackProp(key)把每次属性访问记入集合(见 packages/query-core/src/queryObserver.ts#L258-L273)。
若想关闭该优化,将notifyOnChangeProps设为'all':任何查询更新(新数据、fetching 状态变化等)都会重渲染组件。若只关心data或error,可进一步设成['data', 'error']减少渲染次数。值得补充的是,该配置还支持函数形式,源码在通知分支中通过typeof notifyOnChangeProps === 'function' ? notifyOnChangeProps() : notifyOnChangeProps求值(见 packages/query-core/src/queryObserver.ts#L648-L662),可用于在渲染期间动态决定通知范围。
此外 React Query 会批量合并更新:当多个组件订阅同一条查询时,一次状态变更只触发一次应用层渲染。这意味着数据访问越"克制"、组件重渲染越少。
注解 3:Partial Query Matching —— 前缀语义 + 过滤函数任意操作查询组
由于 React Query 使用确定性查询键序列化,你可以在不逐一罗列具体键的情况下批量操作一组查询。例如:
- 刷新所有以
todos为键前缀的查询(无论携带何种变量); - 精确指定"带/不带变量、或含嵌套属性"的查询;
- 甚至传入过滤函数,只匹配满足自定义条件的查询。
支撑它的底层函数是 packages/query-core/src/utils.ts#L236-L269 的partialMatchKey:它递归地只校验"被匹配方(b)中存在哪些键、a 对应位置是否与之相等",因此{ todos: [...] }这一前缀形态可以命中任意更深层的变体键。这是 SWR 仅能以 🔶(需自行实现)支持、而 React Query 能以一等能力提供的重要原因。
注解 4:Pre-usage Query/Mutation Configuration —— 把配置沉淀到"用之前"
这是"使用前即可配置好查询/变更行为"的能力别名。例如:一条查询可以预先配置好默认值(fetcher、staleTime、重试策略等),真正使用处只写useQuery({ queryKey }),无需每次重复传入 fetcher 或选项。SWR 只有全局默认 fetcher 的"部分形态",既不能按查询粒度配置,也谈不上为 mutation 配置。
在本仓库中,这一能力以多重形态落地:
queryOptions()/mutationOptions()工厂(见 packages/react-query/src/queryOptions.ts、packages/react-query/src/mutationOptions.ts),把查询定义与调用点解耦、同时获得完整类型推断;- 也可通过 QueryClient 的
defaultOptions做全局/键粒度默认值,见 packages/query-core/src/queryClient.ts; - API 定义位置(表格第 11 行)中 React Query 标为 "Component, External Config",即组件内联定义与外部集中定义都受支持,正源于此。
注解 5:Automatic Refetch after Mutation —— "真正自动"依赖 Schema
要实现真正意义上的"变更后自动重取",库需要依赖Schema(例如 GraphQL 提供的 schema)及识别实体的启发式规则,才能知道一次变更影响了哪些实体、进而重取相关查询。因此 Apollo(基于 GraphQL Schema + 规范化缓存)能标 ✅,而 React Query、SWR、RTK Query 均不提供这种魔法式的全自动刷新;React Query 给出的路径是invalidateQueries等显式缓存失效 API,属于"半自动、可控性优先"的设计哲学(标 🔶)。
注解 6:Normalized Caching —— 刻意不做的规范化
React Query、SWR、RTK Query目前都不支持自动规范化缓存(即以扁平实体存储避免高层数据重复)。React Query 的定位是把"数据获取 + 缓存生命周期"做好,实体关系建模仍交给开发者(如用 selector 做 denormalize)。这也解释了表格中 React Query 的 Cache Persistence、Dehydration/Rehydration、离线能力为何全部 ✅ —— 因为它缓存的是完整查询响应而非依赖 Schema 的碎片实体,天然易于整体持久化。
注解 7:SWR 的 Immutable Mode —— 不替代 staleTime
SWR 自带 "immutable" 模式,可让查询在缓存生命周期内只 fetch 一次,但它没有 stale-time 概念,也没有条件式自动重新验证。也就是说,SWR 无法表达"数据在 X 毫秒内视为新鲜、过期后按需重取"这类时间窗口语义;React Query 的staleTime与 RR(stale-while-revalidate)模型是独立的、按查询可配置的。
注解 8:React Router 的缓存持久化边界
React Router不会缓存超出"当前匹配路由"之外的数据:一旦离开某条路由,其 loader 数据即被丢弃(Nested Route → value 模型决定),因此没有跨页面缓存,Cache Persistence、Devtools、轮询、Infinite、Cache Manipulation 等持续型能力自然为 🛑。
React Query 优势特性在源码中的落点
对选型者而言,看懂"React Query 为什么能在多数行拿 ✅"比背表格更有价值。以下能力在本仓库均可直接定位到实现文件。
确定性缓存键:稳定序列化的hashKey
React Query 的缓存键变更检测依赖键本身被稳定序列化。默认哈希函数 packages/query-core/src/utils.ts#L219-L234 的hashKey实现方式是:对值做JSON.stringify,且在 stringify 的 replacer 中对每个普通对象按键名排序后再输出。因此{ b: 1, a: 2 }与{ a: 2, b: 1 }会生成相同哈希——相同语义的查询键被当作同一缓存条目;开发者还可通过queryKeyHashFn注入自定义哈希。这正是表格 "Cache Change Detection: Deep Compare Keys (Stable Serialization)" 的源码依据。
数据记忆化:replaceEqualDeep的结构共享
React Query 的 Data Memoization 为 "Full Structural Sharing":新数据与旧数据深比较后,能复用的子树直接复用旧引用,避免无意义重渲染。核心函数是 packages/query-core/src/utils.ts#L273 起的replaceEqualDeep:
- 若
a === b(引用相等或原始值相等)直接返回a; - 深比较失败时,仅替换 b 中与 a 不等的子节点,其余子节点保持 a 的引用。
该函数在 utils.test.tsx 中有成组的单元测试覆盖(含原始值、Date、数组、对象、undefined 等各种边界),并可通过查询选项structuralSharing: false关闭,或传入自定义函数(见 queryClient.test.tsx 对structuralSharing自定义的验证)。
渲染追踪与批量更新:Proxy +notifyOnChangeProps
如注解 2 所述,trackResult以 Proxy 记录属性访问(见 queryObserver.ts#L258-L273),QueryObserver在通知时比对"本次实际变化字段"与"组件访问过的字段集合"。相关状态分支集中在 queryObserver.ts#L648-L662,支持'all'、字段数组、函数三种形态。React 适配层(见 packages/react-query/src/useBaseQuery.ts)负责在渲染期间把追踪到的属性传给 observer,形成端到端的按需渲染闭环。
自动垃圾回收:gcTime驱动的 Query 生命周期
React Query 的 Auto Garbage Collection 由 Query 自身的生命周期管理实现:查询在失去所有订阅者后进入倒计时,gcTime(原cacheTime)到期即被清理并释放订阅。源码在 packages/query-core/src/query.ts#L211 通过updateGcTime(this.options.gcTime)将选项写入 query 实例,QueryCache 据此调度回收。这解释了为何 React Query 既能"用后即焚"控制内存,又能配合持久化中间件做离线长期留存。
双向无限查询:getNextPageParam/getPreviousPageParam
Infinite Queries 及"双向"能力由 packages/query-core/src/infiniteQueryBehavior.ts 支撑:前向翻页读取getNextPageParam的返回并推进pageParam,后向翻页则基于getPreviousPageParam与initialPageParam从已有首页向前补页(见该文件 L132-L152 的取参逻辑),并对外暴露hasNextPage/hasPreviousPage判断(L164-L175)。React 层入口为 packages/react-query/src/useInfiniteQuery.ts。SWR/Apollo 需要自行组合实现的方向性逻辑,在这里是内置语义。
取消、预取、缓存操作:QueryClient的统一 API
表格中 Query Cancellation、Prefetching、Cache Manipulation、Dehydration/Rehydration 等 ✅ 项,都收敛在 packages/query-core/src/queryClient.ts 暴露的prefetchQuery、fetchQuery、cancelQueries、removeQueries、invalidateQueries、setQueryData、getQueryCache().find/findAll等 API 上。其行为在 packages/query-core/src/tests/queryClient.test.tsx 中由大量测试锁定,例如prefetchQuery后的缓存命中(L1851 附近)、removeQueries精确/前缀删除(L1941 附近)、cancelQueries配合revert选项(L1986、L2003 附近)。React 侧另有独立于组件的usePrefetchQuery/usePrefetchInfiniteQuery(见 packages/react-query/src/usePrefetchQuery.tsx)。
持久化、Devtools 与框架无关核心
- 持久化 / 离线:
persistQueryClient系列位于 packages/react-query-persist-client/src,底层契约定义在 packages/query-persist-client-core/src,配合 packages/query-sync-storage-persister 等存储适配器即可实现 Cache Persistence、Dehydration/Rehydration 与 Offline Caching。 - Devtools:独立包 packages/react-query-devtools/src,另有 packages/query-devtools 承载核心面板,Devtools 一行 ✅。
- Abstracted/Agnostic Core:这是 React Query 架构上区别于 SWR/React Router 的关键点。本仓库根下 packages/query-core 是纯框架无关核心,而 packages/react-query、packages/solid-query、packages/svelte-query、packages/vue-query、packages/preact-query、packages/angular-query-experimental 只是薄薄的框架适配层。查询键、缓存、垃圾回收、重试、轮询等复杂逻辑只写一遍,各框架共享同一套测试与语义——这正是对比文档在同仓库语境下称 React Query 具备 "Abstracted/Agnostic Core" 的直接依据。
如何利用这份对比做决策
把表格与注解还原为决策建议时,请基于自身约束而非"谁的行最多":
- 需要类 Schema 规范化缓存 + GraphQL 深度集成的项目→ Apollo Client 的 Normalized Caching 与变更后自动重取是结构性优势;
- 已经全面使用 Redux、希望状态与数据获取同源管理→ RTK Query 与 Redux 生态集成最顺滑,且同样是外部配置式 API;
- 团队只需一个极轻量的 fetch-on-render 层→ SWR 体积与心智负担更小,但要接受没有 staleTime、GC、Query Cancellation 等进阶能力;
- 数据生命周期应当跟随路由、以 loader 驱动页面数据→ React Router 的嵌套路由数据模型最匹配,但要清楚"离开路由即失缓存"的边界(注解 8);
- 需要 Server-State 完整生命周期(缓存、失效、后台刷新、GC、持久化、并发/依赖/无限查询、Devtools)且希望知识沉淀为框架无关资产→ React Query 的能力矩阵与架构(QueryClient API、queryOptions 预配置、partial key 失效、structural sharing、Suspense 支持等)提供了当前对比中最完整且最可组合的集合。
需要特别强调的是:任何第三方能力声明(例如 SWR 内部基于stable-hash的深比较)均以各库官方文档与源码为准;本文中的 React Query 侧结论均有上文标注的本仓库文件可作为一手证据。同时,"无偏"也意味着承认其边界——如注解 6 所述,React Query刻意不提供自动规范化缓存,这与 Apollo 是取舍而非高下。
结语
一份横向对比表的真正价值,不在于"谁赢了几行 ✅",而在于它逼你直面五个关键架构分叉:缓存是键值、层级还是规范化?数据生命周期绑定查询还是路由?序列化是否稳定、变更检测是深比较还是引用相等?更新是否批量、渲染是否可按属性订阅?核心是否框架无关、能力是否可跨框架复用?以本仓库 docs/framework/react/comparison.md 为基础,结合 query-core 的源码实现,你可以把表中的每一个符号都还原成"一段可以阅读、可以测试、可以评估的真实设计",从而为自己的项目做出有依据的工程决策。
【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考