React篇 | 服务器组件
前言
如果你最近关注 React 生态,一定听说过 Server Components(RSC)。这可能是 React 自 Hooks 以来最大的范式转变。
但很多开发者对 RSC 的理解停留在”在服务端渲染的组件”这个层面,和传统的 SSR 混为一谈。面试中一追问就暴露了:
- Server Components 和 SSR 到底有什么区别?不都是在服务端运行吗?
- “use client” 和 “use server” 这两个指令到底是什么意思?
- Server Components 为什么不能用 useState、useEffect?
- RSC Payload 是什么?和 HTML 有什么区别?
- Server Actions 是什么?为什么需要它?
- 什么组件应该是 Server Component,什么应该是 Client Component?
本章就来把 React Server Components 的核心概念、工作原理、和实际使用策略讲清楚。
诊断自测
Q1:下面哪些组件可以是 Server Component?哪些必须是 Client Component?
// A:显示用户名
function UserName({ name }) {
return <h1>{name}</h1>;
}
// B:带点击计数的按钮
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c + 1)}>{count}</button>;
}
// C:从数据库读取数据并显示
async function UserList() {
const users = await db.query('SELECT * FROM users');
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
点击查看答案
- A:可以是 Server Component。纯展示组件,没有交互,没有 state 和 effect
- B:必须是 Client Component。使用了
useState和事件处理器(onClick),需要在浏览器中运行 - C:可以(且应该)是 Server Component。直接访问数据库是 Server Component 的典型场景——这在客户端是不可能做到的
Q2:“use client” 是什么意思?它标记的是单个组件还是一个边界?
点击查看答案
"use client" 是一个边界标记(boundary),放在文件顶部。它的意思不是”这个文件里的组件在客户端运行”,而是**“从这个文件开始,以及这个文件 import 的所有模块,都属于客户端模块图”**。
它标记的是一个客户端边界的入口。在这个边界之上的组件可以是 Server Component,之下的都是 Client Component。
Q3:Server Components 和 SSR(服务端渲染)有什么区别?
点击查看答案
三个关键区别:
- SSR 输出 HTML,客户端需要注水(hydrate)才能变成可交互的。RSC 输出的是RSC Payload(一种序列化的组件树描述),客户端用它来构建/更新 React 树,不需要 hydrate Server Component 部分
- SSR 的组件代码仍然被发送到客户端(因为需要 hydrate),RSC 的代码不会被发送到客户端——这意味着 Server Component 的代码不计入客户端 bundle 大小
- SSR 只在首次请求时运行(生成初始 HTML),RSC 可以在后续导航中重新运行(通过 RSC Payload 实现增量更新)
简单说:SSR 是”在服务端生成 HTML”,RSC 是”在服务端运行组件逻辑”。两者可以结合使用。
一、Server Components vs Client Components
1.1 两种组件的本质区别
在 RSC 架构下,React 组件被分为两种:
| 特性 | Server Component | Client Component |
|---|---|---|
| 运行环境 | 仅在服务端 | 在客户端(也可能在服务端 SSR) |
| 能否用 state / effect | 不能 | 能 |
| 能否用事件处理器 | 不能 | 能 |
| 能否直接访问服务端资源 | 能(数据库、文件系统) | 不能 |
| 代码是否发送到客户端 | 不发送 | 发送 |
| 默认身份 | 默认就是 | 需要 “use client” 标记 |
1.2 默认行为
在 RSC 架构中,组件默认是 Server Component。只有当你在文件顶部加了 "use client" 时,该文件中的组件才会变成 Client Component。
// 这是一个 Server Component(默认)
async function ProductPage({ id }) {
const product = await db.products.findById(id);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<AddToCartButton productId={id} /> {/* Client Component */}
</div>
);
}
// AddToCartButton.jsx
"use client"; // ← 标记为 Client Component
import { useState } from 'react';
export function AddToCartButton({ productId }) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => setAdded(true)}>
{added ? 'Added!' : 'Add to Cart'}
</button>
);
}
1.3 Server Component 的能力
Server Component 可以做到很多 Client Component 做不到的事情:
// ✅ 直接访问数据库
async function UserProfile({ userId }) {
const user = await prisma.user.findUnique({ where: { id: userId } });
return <div>{user.name}</div>;
}
// ✅ 读取文件系统
import { readFile } from 'fs/promises';
async function MarkdownPage({ slug }) {
const content = await readFile(`./content/${slug}.md`, 'utf-8');
return <article dangerouslySetInnerHTML={{ __html: marked(content) }} />;
}
// ✅ 使用仅服务端的 npm 包(不会增加客户端 bundle)
import { highlight } from 'shiki'; // 语法高亮库,几 MB 大
async function CodeBlock({ code, lang }) {
const html = await highlight(code, { lang });
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
1.4 Server Component 的限制
// ❌ 不能用 useState
function ServerComp() {
const [count, setCount] = useState(0); // 报错
}
// ❌ 不能用 useEffect
function ServerComp() {
useEffect(() => { /* ... */ }, []); // 报错
}
// ❌ 不能用事件处理器
function ServerComp() {
return <button onClick={() => {}}> // 报错
Click
</button>;
}
// ❌ 不能用浏览器 API
function ServerComp() {
const width = window.innerWidth; // 报错,服务端没有 window
}
为什么有这些限制? 因为 Server Component 只在服务端运行一次(渲染时),不会在客户端再运行。useState 需要在客户端维护状态、useEffect 需要在 DOM 挂载后执行、事件处理器需要在用户交互时触发——这些都是客户端的事情,Server Component 做不到。
二、“use client” 和 “use server” 指令
2.1 “use client”:客户端边界
"use client" 不是一个”装饰器”,而是一个模块级别的边界声明。
"use client";
// 从这行开始,这个文件里的所有导出都是 Client Component
// 这个文件 import 的所有模块也被视为客户端模块
关键理解:"use client" 标记的是边界的入口,不是单个组件。
Server Component Tree
│
├── ServerLayout
│ ├── ServerHeader
│ └── "use client" boundary ─── ClientNav (+ 它 import 的所有模块)
│
└── ServerContent
└── "use client" boundary ─── ClientForm (+ 它 import 的所有模块)
2.2 Server Component 可以渲染 Client Component
这是一个容易混淆的点:Server Component 可以 将 Client Component 作为子组件渲染:
// ServerPage.jsx (Server Component)
import { ClientCounter } from './ClientCounter';
async function ServerPage() {
const data = await fetchData();
return (
<div>
<h1>{data.title}</h1>
{/* Server Component 渲染 Client Component */}
<ClientCounter initialCount={data.count} />
</div>
);
}
但 Client Component 不能 import Server Component:
// ClientWidget.jsx
"use client";
// ❌ 不能这样做
import { ServerOnlyComponent } from './ServerOnlyComponent';
不过 Client Component 可以通过 children 接收 Server Component:
// ClientLayout.jsx
"use client";
export function ClientLayout({ children }) {
const [sidebarOpen, setSidebarOpen] = useState(true);
return (
<div>
<button onClick={() => setSidebarOpen(!sidebarOpen)}>Toggle</button>
{sidebarOpen && <aside>Sidebar</aside>}
<main>{children}</main> {/* children 可以是 Server Component */}
</div>
);
}
// ServerPage.jsx (Server Component)
import { ClientLayout } from './ClientLayout';
async function ServerPage() {
const content = await fetchContent();
return (
<ClientLayout>
{/* 这里是 Server Component,通过 children 传入 */}
<ServerContent data={content} />
</ClientLayout>
);
}
2.3 “use server”:Server Actions
"use server" 和 "use client" 不是对称的。"use client" 标记客户端模块边界,"use server" 标记的是 Server Action——可以从客户端调用的服务端函数。
// actions.js
"use server";
export async function addToCart(productId) {
// 这个函数在服务端执行
const cart = await db.carts.findFirst({ where: { userId: currentUser.id } });
await db.cartItems.create({
data: { cartId: cart.id, productId },
});
}
// ClientButton.jsx
"use client";
import { addToCart } from './actions';
export function AddToCartButton({ productId }) {
return (
<button onClick={() => addToCart(productId)}>
Add to Cart
</button>
);
}
当客户端调用 addToCart 时,React 会自动发送一个 HTTP 请求到服务端,执行这个函数,然后返回结果。你不需要手动创建 API 路由。
三、RSC 的数据流
3.1 首次加载
用户首次访问页面时的流程:
1. 浏览器请求页面
2. 服务端运行 Server Components,生成 RSC Payload
3. 服务端同时做 SSR,生成初始 HTML(用于快速显示)
4. 浏览器收到 HTML,立即显示(静态内容)
5. 浏览器下载 Client Component 的 JavaScript bundle
6. React 在客户端 hydrate Client Components(让它们变得可交互)
7. Server Components 不需要 hydrate(它们不在客户端运行)
3.2 RSC Payload 是什么?
RSC Payload 是 Server Component 渲染结果的序列化表示。它不是 HTML,而是一种特殊的格式,描述了组件树的结构:
// 简化的 RSC Payload 示例(实际格式是流式的)
0: ["$", "div", null, {
"children": [
["$", "h1", null, { "children": "Product Name" }],
["$", "$Lazy", null, { /* Client Component 引用 */ }]
]
}]
RSC Payload 包含:
- Server Component 的渲染结果(类似虚拟 DOM 的序列化)
- Client Component 的引用(指向 JS bundle 中的模块)
- 传给 Client Component 的 props(必须是可序列化的)
3.3 后续导航
用户在应用内导航时(如点击链接),RSC 的行为和首次加载不同:
1. 客户端向服务端请求新路由的 RSC Payload
2. 服务端运行新路由的 Server Components,生成 RSC Payload
3. 客户端接收 RSC Payload,更新 React 树
4. 不需要完整的页面 HTML
5. Client Components 的状态可以保留(如果它们在新旧路由中都存在)
这就是 RSC 和传统 SSR 的关键区别之一:SSR 在后续导航中退化为客户端渲染(或需要完整的页面重载),而 RSC 可以在后续导航中继续利用服务端能力。
3.4 props 必须可序列化
Server Component 传给 Client Component 的 props 必须是可序列化的——因为这些 props 需要通过 RSC Payload 从服务端传输到客户端。
// ✅ 可序列化的 props
<ClientComp
name="Alice"
count={42}
items={['a', 'b', 'c']}
config={{ theme: 'dark' }}
/>
// ❌ 不可序列化的 props
<ClientComp
onClick={() => console.log('click')} // 函数不可序列化
ref={someRef} // ref 不可序列化
element={<ServerOnlyComp />} // Server Component 实例不可直接传
/>
但有一个例外:Server Actions 可以作为 props 传递。React 会自动把它们序列化为一个引用(endpoint),客户端调用时会发送请求到服务端。
// ✅ Server Action 可以传给 Client Component
async function ServerForm() {
async function submitForm(formData) {
"use server";
await db.forms.create({ data: Object.fromEntries(formData) });
}
return <ClientForm onSubmit={submitForm} />;
}
四、Server Actions
4.1 什么是 Server Actions?
Server Actions 是可以从客户端直接调用的服务端函数。它们解决的核心问题是:让表单提交、数据变更等操作不需要手动创建 API 路由。
// 传统方式:需要创建 API 路由
// /api/submit.js
export async function POST(req) {
const data = await req.json();
await db.save(data);
return Response.json({ success: true });
}
// 客户端
const handleSubmit = async (data) => {
await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(data),
});
};
// Server Actions 方式:直接定义函数
async function submitAction(formData) {
"use server";
const name = formData.get('name');
await db.users.create({ data: { name } });
}
function SignupForm() {
return (
<form action={submitAction}>
<input name="name" />
<button type="submit">Sign Up</button>
</form>
);
}
4.2 Server Actions 的工作原理
当客户端调用一个 Server Action 时,React 会:
- 序列化函数参数
- 发送 HTTP POST 请求到服务端
- 服务端执行函数
- 返回结果(包括可能的 UI 更新)
- 客户端更新 React 树
整个过程对开发者是透明的——你只需要写一个 async 函数,React 处理所有网络通信。
4.3 配合 useActionState
React 提供了 useActionState(之前叫 useFormState)来管理 Server Action 的状态:
"use client";
import { useActionState } from 'react';
import { createUser } from './actions';
function SignupForm() {
const [state, formAction, isPending] = useActionState(createUser, {
error: null,
success: false,
});
return (
<form action={formAction}>
<input name="name" disabled={isPending} />
<button type="submit" disabled={isPending}>
{isPending ? 'Submitting...' : 'Sign Up'}
</button>
{state.error && <p className="error">{state.error}</p>}
{state.success && <p>Account created!</p>}
</form>
);
}
五、RSC 与 SSR 的区别
这是面试中必考的对比题,很多人会混淆。
5.1 关键区别
| 维度 | SSR | RSC |
|---|---|---|
| 输出 | HTML 字符串 | RSC Payload(序列化组件树) |
| 客户端 JS | 所有组件的 JS 都要发送 | 只发送 Client Component 的 JS |
| Hydration | 整个页面需要 hydrate | 只有 Client Components 需要 hydrate |
| 后续导航 | 退化为 CSR 或完整页面刷新 | 可以请求新的 RSC Payload |
| 运行时机 | 只在首次请求时运行 | 每次渲染都可以在服务端运行 |
| 状态交互 | Hydrate 后可以有状态 | Server Component 本身没有状态 |
5.2 它们不是互斥的
RSC 和 SSR 不是二选一,它们通常一起使用:
首次请求流程(RSC + SSR):
1. 服务端运行 Server Components → 生成 RSC Payload
2. 用 RSC Payload + Client Component 代码做 SSR → 生成 HTML
3. HTML 发送到浏览器 → 用户立即看到内容
4. Client Component JS 加载并 hydrate → 页面变得可交互
- RSC 负责”哪些逻辑在服务端运行”
- SSR 负责”首次加载时生成 HTML 以加快首屏显示”
5.3 一个直观的类比
想象一个电商产品页面:
- 纯 CSR:浏览器下载空 HTML → 下载 JS → 执行 JS → 请求数据 → 渲染页面。用户看到白屏好几秒
- SSR:服务端渲染完整 HTML → 浏览器显示 → 下载 JS → hydrate → 可交互。首屏快但 JS bundle 大
- RSC + SSR:Server Component 直接查数据库渲染产品信息(代码不发到客户端)→ Client Component 只负责”加入购物车”按钮(少量 JS)→ 首屏快且 JS 更小
常见误区
误区一:“Server Components 就是 SSR 的新叫法”
完全不是。SSR 是把组件渲染成 HTML 字符串的过程,所有组件代码仍然要发送到客户端做 hydration。RSC 是一种新的组件类型,它的代码永远不会出现在客户端,运行结果以 RSC Payload 的形式传输。两者解决的问题不同,且通常一起使用。
误区二:“‘use client’ 意味着组件不会在服务端运行”
不对。标记了 "use client" 的组件仍然可能在服务端做 SSR(生成初始 HTML)。"use client" 的意思是”这个组件需要在客户端 hydrate 并运行”,而不是”这个组件只在客户端运行”。在首次请求时,Client Component 可能同时在服务端(SSR)和客户端(hydration)运行。
误区三:“所有组件都应该尽量是 Server Component”
虽然 Server Component 有很多优势,但不是所有组件都适合。需要交互(state、事件、动画)、需要浏览器 API(localStorage、window)、需要实时更新的组件,都应该是 Client Component。正确的策略是:让交互边界尽量靠近叶子节点——把需要交互的最小部分标记为 “use client”,其他部分保持为 Server Component。
误区四:“Server Actions 就是 API 路由”
虽然底层都是 HTTP 请求,但 Server Actions 和手写 API 路由有本质区别:Server Actions 与 React 的渲染系统深度集成——调用 Server Action 后,React 可以自动重新渲染受影响的 Server Components,更新 UI,无需手动管理状态同步。API 路由是通用的 HTTP 端点,需要你自己处理所有这些。
小结
本章我们系统讲解了 React Server Components 的核心概念和工作原理。
核心要点
- Server Component 默认:组件默认是 Server Component,需要交互时用 “use client” 标记
- “use client” 是边界:标记的是客户端模块图的入口,不是单个组件
- RSC Payload:Server Component 的渲染结果,不是 HTML,是序列化的组件树
- Server Actions(“use server”):可从客户端调用的服务端函数,替代手写 API 路由
- RSC 不等于 SSR:RSC 是组件类型和运行位置的划分,SSR 是首屏 HTML 生成的技术
- 两者协作:RSC 决定”什么在服务端运行”,SSR 提供”首屏快速展示”
- props 可序列化:Server → Client 的 props 必须可以 JSON 序列化(Server Actions 除外)
本章思维导图
- Server Component vs Client Component
- 运行环境:服务端 vs 客户端
- 能力差异:数据库/文件系统 vs state/effect/事件
- 代码分发:不发送 vs 发送到客户端
- 默认身份:默认 Server / "use client" 标记 Client
- "use client" 指令
- 模块级边界声明
- 标记客户端模块图的入口
- Server 可以渲染 Client(作为子组件)
- Client 不能 import Server(但可以通过 children)
- "use server" 指令
- 标记 Server Action
- 可从客户端调用的服务端函数
- React 自动处理网络通信
- RSC 数据流
- 首次加载:RSC Payload + SSR HTML + Hydration
- RSC Payload:序列化的组件树描述
- 后续导航:请求新的 RSC Payload,增量更新
- props 必须可序列化
- Server Actions
- 替代手写 API 路由
- 与 form action 集成
- useActionState 管理状态
- RSC vs SSR
- 输出不同:RSC Payload vs HTML
- 代码分发不同:不发送 vs 需要发送
- 不互斥,通常一起使用
练习挑战
第一题 ⭐(基础):判断组件类型
对于以下场景,判断应该使用 Server Component 还是 Client Component,并说明原因:
- 博客文章页面(从数据库读取 Markdown 内容并渲染)
- 点赞按钮(用户点击后计数 +1,需要动画效果)
- 导航栏(显示固定的链接列表)
- 搜索框(用户输入时实时过滤结果)
点击查看答案与解析
- Server Component。直接在服务端读取数据库、解析 Markdown,代码(如 Markdown 解析库)不需要发送到客户端
- Client Component。需要 useState 管理计数、onClick 处理点击、动画需要在浏览器中运行
- Server Component。纯展示,没有交互逻辑。如果导航栏有”当前高亮”效果需要读取路由状态,可能需要 Client Component
- Client Component。需要 useState 管理输入值、onChange 处理输入事件、实时过滤需要在客户端执行
思考题:对于第 2 个场景,如果点赞数需要持久化到数据库,应该怎么做?答案是组合使用——点赞按钮是 Client Component,但可以调用 Server Action 来更新数据库。
第二题 ⭐⭐(进阶):重构组件结构
下面的组件全都是 Client Component。请重构它,把能放到服务端的部分提取为 Server Component,只保留必要的 “use client” 部分。
"use client";
import { useState } from 'react';
export function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
const [inCart, setInCart] = useState(false);
useEffect(() => {
fetch(`/api/products/${productId}`)
.then(r => r.json())
.then(setProduct);
}, [productId]);
if (!product) return <Loading />;
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>Price: ${product.price}</p>
<img src={product.image} alt={product.name} />
<button onClick={() => setInCart(true)}>
{inCart ? 'In Cart' : 'Add to Cart'}
</button>
</div>
);
}
点击查看答案与解析
// ProductPage.jsx (Server Component - 不需要 "use client")
import { AddToCartButton } from './AddToCartButton';
async function ProductPage({ productId }) {
// 直接在服务端获取数据,不需要 useEffect + fetch
const product = await db.products.findById(productId);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>Price: ${product.price}</p>
<img src={product.image} alt={product.name} />
<AddToCartButton productId={productId} />
</div>
);
}
// AddToCartButton.jsx (Client Component)
"use client";
import { useState } from 'react';
export function AddToCartButton({ productId }) {
const [inCart, setInCart] = useState(false);
return (
<button onClick={() => setInCart(true)}>
{inCart ? 'In Cart' : 'Add to Cart'}
</button>
);
}
重构要点:
- 数据获取移到 Server Component,直接 await 数据库查询,去掉 useEffect + fetch
- 交互逻辑(加购按钮)提取为独立的 Client Component
- 纯展示部分(标题、描述、价格、图片)保留在 Server Component
- 结果:客户端 JS 只包含一个按钮组件的代码,大幅减少 bundle 大小
第三题 ⭐⭐⭐(综合):实现一个带 Server Action 的表单
实现一个用户注册表单,要求:
- 表单本身是 Client Component(需要管理 loading 和错误状态)
- 提交逻辑是 Server Action(在服务端验证数据并写入数据库)
- 使用
useActionState管理状态 - 处理验证错误
点击查看参考实现
// actions.js
"use server";
export async function registerUser(prevState, formData) {
const name = formData.get('name');
const email = formData.get('email');
const password = formData.get('password');
// 服务端验证
if (!name || name.length < 2) {
return { error: 'Name must be at least 2 characters', success: false };
}
if (!email || !email.includes('@')) {
return { error: 'Invalid email address', success: false };
}
if (!password || password.length < 8) {
return { error: 'Password must be at least 8 characters', success: false };
}
// 检查邮箱是否已存在
const existing = await db.users.findFirst({ where: { email } });
if (existing) {
return { error: 'Email already registered', success: false };
}
// 创建用户
await db.users.create({
data: { name, email, password: await hash(password) },
});
return { error: null, success: true };
}
// RegisterForm.jsx
"use client";
import { useActionState } from 'react';
import { registerUser } from './actions';
export function RegisterForm() {
const [state, formAction, isPending] = useActionState(registerUser, {
error: null,
success: false,
});
if (state.success) {
return <p>Registration successful! Please log in.</p>;
}
return (
<form action={formAction}>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" required disabled={isPending} />
</div>
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" required disabled={isPending} />
</div>
<div>
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" required disabled={isPending} />
</div>
{state.error && <p className="error">{state.error}</p>}
<button type="submit" disabled={isPending}>
{isPending ? 'Registering...' : 'Register'}
</button>
</form>
);
}
核心要点:
- Server Action 接收
(prevState, formData)两个参数 - 返回新的状态(包含错误信息或成功标志)
useActionState自动管理 pending 状态和表单状态- 验证逻辑在服务端执行,客户端只负责 UI 展示
自我检测
读完本章后,对照下面的清单检验一下自己的掌握程度。
- 能说出 Server Component 和 Client Component 的至少五个区别
- 能解释 “use client” 是模块边界标记而不是组件标记
- 能描述 RSC Payload 是什么,以及它与 HTML 的区别
- 能说清楚 Server Component 传给 Client Component 的 props 为什么必须可序列化
- 能解释 RSC 和 SSR 的区别,以及它们如何协作
- 能描述 Server Actions 的工作原理(客户端调用 → HTTP 请求 → 服务端执行)
- 在给定场景下能判断应该使用 Server Component 还是 Client Component
- 能将一个全 Client Component 的页面重构为 Server + Client 组合
购买课程解锁全部内容
大厂前端面试通关:71 篇构建完整知识体系
¥89.90