React Server Components与Client Components的边界划分
React Server Components (RSC) 和 Client Components (CC) 的混合使用是现代 React 开发的核心。错误地划分边界会导致应用变慢、交互失效甚至逻辑错误。本文将提供一套严格的判断标准与操作步骤,帮助你精确划分两者的边界。
核心原则:默认服务端,按需客户端
在 React App Router 或 Next.js 等现代框架中,所有组件默认都是服务端组件。这意味着你不需要刻意去标记服务端组件,只需要在必须用到浏览器特性时,才将其标记为客户端组件。
第一步:判断是否需要交互性
这是划分边界的首要标准。这里的“交互”特指需要 React Hooks 来管理的状态或副作用。
- 检查组件内部是否使用了
useState、useReducer、useRef等 Hooks。 - 检查组件内部是否使用了
useEffect来处理副作用(如数据获取、DOM 操作)。 - 执行判断:
- 如果上述任意一项为“是”,则该组件必须是客户端组件。
- 打开该文件,在代码最顶端添加
'use client';指令。
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
点击次数: {count}
</button>
);
}
第二步:检查浏览器 API 依赖
有些组件虽然不显式使用 Hooks,但依赖浏览器提供的全局对象。
- 审查代码中是否包含
window、document、navigator等对象。 - 审查是否使用了基于这些对象的第三方库(如
localStorage、Audio、Video或特定的动画库)。 - 执行判断:
- 如果代码直接调用了浏览器 API,则该组件必须是客户端组件。
- 添加
'use client';指令。
第三步:排查事件监听器
React 事件处理函数(如 onClick、onChange、onSubmit)只能在客户端执行。
- 查找 JSX 中的
on*属性。 - 确认这些事件处理函数是否直接定义在当前组件内部,或者作为 props 从父组件传入。
- 执行判断:
- 如果组件内部直接定义了事件处理函数(例如
onClick={() => alert('Hi')}),它必须是客户端组件。 - 注意:如果组件仅渲染 HTML,且事件处理函数完全由父组件(客户端组件)传入,那么当前组件可以是服务端组件(但这属于高级用法,初学者建议将含交互的叶子节点都设为客户端)。
- 如果组件内部直接定义了事件处理函数(例如
辅助决策流程图
当你在犹豫该组件属于哪一类时,请严格按照以下流程进行决策。
第四步:对比清单与数据获取模式
理解数据获取方式的差异是优化性能的关键。
| 特性 | Server Components | Client Components |
|---|---|---|
| 默认状态 | 默认开启 | 需添加 'use client' |
| 数据获取 | 直接在组件内使用 async/await |
依赖 useEffect 或 SWR/React Query |
| 网络请求 | 发生在服务端,访问数据库/后端安全 | 发生在浏览器端,需通过 API 调用 |
| 打包体积 | 代码不发送到浏览器,0 KB 体积 | 代码会发送到浏览器,增加包体积 |
| 密钥安全 | 可安全存储 API Key/密钥 | 严禁存储密钥,暴露给用户 |
| 交互能力 | 无(无 Hooks,无事件) | 完整支持 |
数据获取示例对比
服务端组件写法(推荐):
// 这是一个 Server Component,无需 'use client'
async function UserProfile({ id }: { id: string }) {
// 直接后端获取数据
const res = await fetch(`https://api.example.com/user/${id}`);
const user = await res.json();
return <div>用户: {user.name}</div>;
}
```
**客户端组件写法(仅必要时使用):**
```tsx
'use client';
import { useEffect, useState } from 'react';
function UserProfile({ id }: { id: string }) {
const [user, setUser] = useState(null);
useEffect(() => {
// 浏览器端获取数据
fetch(`https://api.example.com/user/${id}`)
.then(res => res.json())
.then(data => setUser(data));
}, [id]);
if (!user) return <div>加载中...</div>;
return <div>用户: {user.name}</div>;
}
第五步:处理组件嵌套与 Props 传递
最复杂的场景是服务端组件与客户端组件互相嵌套。记住一条黄金法则:数据自上而下流动,交互自下而上隔离。
场景 1:Server 包裹 Client(常见)
布局是 Server Component,其中的按钮是 Client Component。
- 保留父级文件为默认(服务端)。
- 创建子组件文件,添加
'use client'。 - 在服务端组件中导入并使用客户端组件。
// 父组件: layout.tsx (Server Component)
import InteractiveButton from './InteractiveButton';
export default function Layout() {
return (
<div>
<h1>服务端渲染的标题</h1>
<InteractiveButton />
</div>
);
}
场景 2:Client 包裹 Server(高级:Props 模式)
客户端组件不能直接导入服务端组件作为子组件渲染。如果客户端组件需要接收服务端生成的复杂 UI 作为子元素,必须通过 children 属性传递。
- 定义客户端组件,使其接收
children属性。 - 定义服务端组件,负责获取数据。
- 在服务端组件中包裹客户端组件,并将服务端生成的 JSX 作为
children传入。
// 1. 客户端组件: ClientWrapper.tsx
'use client';
export default function ClientWrapper({ children }: { children: React.ReactNode }) {
const [show, setShow] = useState(false);
return (
<div>
<button onClick={() => setShow(!show)}>切换显示</button>
{show && <div className="client-style">{children}</div>}
</div>
);
}
// 2. 服务端组件: page.tsx
import ClientWrapper from './ClientWrapper';
async function ServerData() {
// 模拟数据库查询
return <div>这是从数据库获取的敏感数据:$1234</div>;
}
export default function Page() {
return (
<ClientWrapper>
{/* ServerData 在服务端渲染,结果作为 props 传给 ClientWrapper */}
<ServerData />
</ClientWrapper>
);
}
关键点:
- 严禁在 Client Component 内部
import一个 Server Component 直接使用<ServerComponent />。 - 必须使用
<ClientWrapper>{<ServerComponent />}</ClientWrapper>的形式。
第六步:优化第三方库的使用
许多第三方库(如 UI 组件库 antd、Material UI,或动画库 Framer Motion)内部依赖浏览器 API 或 Hooks。
- 查阅第三方库文档,确认其是否支持 Server Components。
- 如果不支持,你必须创建一个客户端组件来包装该库,然后在上层的服务端组件中引用这个包装器。
- 示例:使用一个不支持 RSC 的滑块组件。
// 1. 创建包装器: SliderWrapper.tsx
'use client';
import { Slider } from 'some-third-party-lib';
export default function SliderWrapper(props: any) {
return <Slider {...props} />;
}
// 2. 在服务端组件中使用
import SliderWrapper from './SliderWrapper';
export default function Page() {
return (
<main>
<SliderWrapper min={0} max={100} />
</main>
);
}
通过以上步骤,你可以建立起清晰的组件边界思维:尽可能让代码在服务端运行以获得最佳性能,仅在涉及用户交互和浏览器特性时才将必要的部分下放到客户端。

暂无评论,快来抢沙发吧!