最近几年,前端数据获取这块的讨论特别热闹。先是 React Query 和 SWR 几乎同时出现,把客户端异步状态管理推到了一个新高度,然后 React Server Components(RSC)又横空出世,让很多人一下子懵了:既然可以在服务端直接查数据,那以前的客户端数据请求库是不是就过时了?

真正让人纠结的不是这些技术本身,而是它们之间的边界到底在哪里。尤其当你在一个真实项目里既要考虑首屏性能,又要处理用户交互、实时更新、乐观更新这些场景时,就会发现,单独用任何一个都差点意思。这篇文章想聊的,就是这三者到底在解决什么问题,哪些场景下该用谁,以及它们是怎样一层层叠加成一套新的数据获取范式的。
为什么会有 React Query 和 SWR
在 React 里做数据请求,最原始的方式无非是在 useEffect 里 fetch,然后存到 useState 里。这种写法的问题不是跑不起来,而是当项目规模稍微大一点,就会暴露出大量重复的样板代码:加载状态、错误处理、缓存、去重请求、组件卸载时的竞态问题……每个开发者都得自己封装一遍,而且很难做好。
React Query 和 SWR 本质上是在解决同一个问题:把服务端数据当作一份需要被“缓存”和“同步”的异步状态来管理。它们都提供了:
- 以 key 为基础的请求去重与缓存
- 窗口聚焦时自动重新拉取(stale-while-revalidate)
- 后台数据更新与乐观更新
- 分页、无限滚动等复杂场景的支持
但这两个库不是简单的轮子,它们的核心价值在于把“服务端状态”和“客户端 UI 状态”彻底分开。一旦你开始用这种方式组织代码,很多原本需要手动处理的副作用(比如请求状态同步、数据失效)就变成了声明式的配置,这在中大型项目中省下的不止是代码量,而是整个团队的认知成本。
RSC 带来的变化
React Server Components 则走了另一条路。它允许组件直接在服务端运行,可以在渲染过程中直接访问数据库、文件系统,或者调用那些不该暴露给客户端的 API。这意味着很多数据根本不需要通过客户端请求来获取——组件在服务端跑完,把结果序列化成 React 的虚拟 DOM 结构丢给客户端,客户端只负责展示和交互。
RSC 最吸引人的地方在于:
- 零客户端请求的数据获取,天然没有 loading 闪烁
- 可以直接访问后端资源,没有 API 层的开销
- 大幅减少客户端 JavaScript 体积
但它的限制也很明显:服务端组件没有交互性,不能使用 useState、useEffect 这些 hooks,也无法响应用户的点击、输入等操作。换句话说,RSC 擅长的是“一次性”的数据展示,而一旦涉及需要持续变更的数据,它就力不从心了。
一个常见的误区
很多人看完 RSC 的演示后,会觉得 React Query 和 SWR 要退出历史舞台了。这个想法过于简单。实际上,RSC 并没有解决客户端数据获取库最擅长的那部分问题——它只是把一部分数据获取的时机从客户端移到了服务端。
真正需要清醒认识的是:一个应用里存在两类完全不同的数据。
- 首屏数据:用来渲染页面骨架,通常相对静态,或者能接受一定延迟的更新。
- 交互数据:用户操作后需要实时反映到界面的数据,比如表单提交、搜索、列表筛选、实时通知等。
RSC 完美覆盖了第一类,但第二类数据的处理依然需要客户端的能力。React Query 和 SWR 在这里的价值不但没有消失,反而因为客户端需要处理更复杂的状态同步而变得更重要。
真实场景下的共存方式
假设你正在做一个电商的商品详情页。商品的基本信息(标题、价格、库存)非常适合用 RSC 在服务端直接获取,这样页面打开就能看到完整内容,没有任何 loading 过程。但“加入购物车”这个操作涉及的数据——比如购物车商品数量、最新价格校验——就需要客户端实时处理和反馈。
这时一个自然的模式是:RSC 负责把初始数据渲染出来,同时将这些数据“注入”到客户端的 React Query 缓存中,作为初始值。后续用户在页面上的交互,由 React Query 接管,负责增量更新和缓存管理。
下面是一个简化的代码示意,展示这种注入方式:
// 服务端组件(RSC)
import { getProduct } from '@/db';
export default async function ProductPage({ id }) {
const product = await getProduct(id);
return (
);
}
// 客户端组件
'use client';
import { useQuery } from '@tanstack/react-query';
function ClientProductDetail({ initialData }) {
const { data } = useQuery({
queryKey: ['product', initialData.id],
queryFn: () => fetchProduct(initialData.id),
initialData, // 服务端数据直接成为缓存
});
// ... 后续交互逻辑
}
这种做法让服务端和客户端的数据层共享了同一份缓存结构,避免了重复请求,也保证了交互的响应速度。对于需要更复杂处理的场景,还可以利用 hydrate 机制,将服务端预取的数据完整地注入到 QueryClient 中。
不同方案的对比
为了更清晰地看到它们各自适合的领域,我整理了一份对比:
| 维度 | React Query / SWR | RSC | 混合使用 |
|---|---|---|---|
| 数据获取位置 | 客户端 | 服务端 | 服务端 + 客户端 |
| 缓存控制 | 客户端内存,可精细控制 | 无(每次渲染重新获取) | 客户端缓存,服务端提供初始数据 |
| 交互性 | 完全支持 | 不支持 | 支持 |
| 实时更新 | 轮询、订阅、乐观更新 | 需通过刷新页面或重新请求 | 客户端实时,服务端首屏 |
| SEO 友好 | 需配合 SSR | 天然友好 | 天然友好 |
| 学习成本 | 中等 | 较高(需理解 RSC 边界) | 较高 |
这份对比不是为了让你选一个,而是说明它们在不同轴上分工明确。一个成熟的团队应该根据组件的数据特征来分配职责,而不是追求“只用一种方案”。
实践中容易踩的坑
虽然理念上很清晰,但在实际落地时,有几个点特别容易出问题:
- 重复请求:服务端已经获取了数据,客户端组件又用 useEffect 重新请求了一次。这通常是因为没有做 initialData 注入,或者 hydration 配置不当。
- 缓存失效不同步:RSC 渲染的数据是那一刻的“快照”,如果客户端缓存因为用户操作更新了,而服务端渲染的框架没有重新渲染,就会出现数据不一致。需要用合理的方式(如 revalidatePath)来让服务端感知变化。
- 组件边界划分不清:把太多交互逻辑放在服务端组件里,导致后期不得不把它转成客户端组件,引发连锁改动。比较好的做法是,先明确哪些部分是纯展示,哪些部分需要交互,然后再决定谁来获取数据。
很多团队一开始会走极端,要么全用 RSC 追求极致性能,要么因为惯性把一切请求都扔给 React Query。实际上,正确的做法往往是在这两个极端之间找到一个平衡点,而且这个平衡点会随着项目迭代而移动。
什么时候该用哪种方案
如果你正在做技术选型,可以参考下面这几个判断:
- 这个数据只在页面初始渲染时使用,且之后很少变化?——优先考虑 RSC 直接获取。
- 数据需要频繁响应用户操作(搜索、过滤、排序)?——用 React Query 或 SWR,并结合 RSC 提供初始数据。
- 需要强实时性,比如聊天、通知?——客户端库是必须的,RSC 无法胜任。
- 团队暂时没有 App Router 或 RSC 的使用条件?——老老实实用 React Query / SWR,它们依然是目前最稳定的客户端数据方案。
关键不在于哪个技术更“新”,而在于你的组件到底需要什么能力。服务端组件擅长减少请求、加快首屏,客户端缓存库擅长管理动态数据、提升交互体验,两者不是替代,而是互补。
新范式的本质
前端数据获取的新范式,并不是用一个新工具取代旧工具,而是数据获取的分层。以前我们习惯把所有数据请求都放在客户端,现在我们可以把数据获取分成三层:静态 / 准静态数据交给服务端,动态交互数据交给客户端缓存库,而全局状态管理(如 Redux)则进一步退后,只处理纯客户端状态。
这种分层让每一层都更专注于自己的职责,也更容易做性能优化。理解了这一点,就不会再纠结 React Query 和 RSC 谁更好,而是会开始思考:我的这个组件,到底该把数据获取放在哪一层,才能让整个应用的可维护性和性能都达到预期。
原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/435/