Appearance
第11章 Server Actions与Hydration
本章将深入React 19的Server Actions机制和Hydration水合过程,理解服务端函数调用、表单处理、选择性水合、不匹配处理等核心技术。这是理解全栈React应用数据流的关键章节。
在前面的章节中,我们学习了React Server Components如何在服务端渲染组件、Flight协议如何传输数据。但在实际应用中,客户端如何调用服务端函数?服务端渲染的HTML如何与客户端React实例关联?如何处理用户交互?
本章将逐一解答这些问题,带你深入理解Server Actions和Hydration的完整流程。
11.1 Server Actions概述
Server Actions是React 19引入的革命性特性,允许客户端直接调用服务端函数,无需手动编写API端点。
11.1.1 什么是Server Actions
为什么需要Server Actions?
在传统的全栈应用中,客户端与服务端的数据交互需要:
- 在服务端定义API路由(如
/api/createUser) - 在客户端使用fetch调用API
- 处理请求/响应的序列化
- 管理加载状态和错误处理
这种模式存在几个问题:
- 代码分散:业务逻辑分散在客户端和服务端
- 类型安全:客户端和服务端的类型容易不一致
- 样板代码:需要大量重复的fetch、错误处理代码
- 网络瀑布:多个数据依赖导致串行请求
Server Actions的解决方案
Server Actions允许你在服务端定义函数,然后在客户端直接调用,就像调用本地函数一样:
javascript
// app/actions.js - 服务端文件
'use server';
export async function createUser(formData) {
const name = formData.get('name');
const email = formData.get('email');
// 直接访问数据库
const user = await db.users.create({ name, email });
return { success: true, userId: user.id };
}javascript
// app/UserForm.jsx - 客户端组件
'use client';
import { createUser } from './actions';
export function UserForm() {
return (
<form action={createUser}>
<input name="name" />
<input name="email" />
<button type="submit">创建用户</button>
</form>
);
}核心优势
- 类型安全:TypeScript可以跨越客户端/服务端边界
- 渐进增强:表单在JavaScript加载前就能工作
- 自动序列化:React自动处理参数和返回值的序列化
- 统一错误处理:服务端错误可以在客户端捕获
11.1.2 "use server"指令
"use server"指令标记一个函数或文件为Server Action。
文件级指令
javascript
// actions.js
'use server';
// 文件中所有导出的函数都是Server Actions
export async function createUser(data) { /* ... */ }
export async function deleteUser(id) { /* ... */ }
export async function updateUser(id, data) { /* ... */ }函数级指令
javascript
// UserList.jsx - Server Component
export async function UserList() {
const users = await db.users.findAll();
// 内联Server Action
async function deleteUser(userId) {
'use server';
await db.users.delete(userId);
revalidatePath('/users');
}
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name}
<form action={deleteUser.bind(null, user.id)}>
<button type="submit">删除</button>
</form>
</li>
))}
</ul>
);
}指令的作用
- 编译时标记:打包工具识别Server Action
- 生成引用:创建可序列化的函数引用
- 网络端点:自动生成HTTP端点
- 安全检查:确保只在服务端执行
11.1.3 与传统API的区别
| 特性 | 传统API | Server Actions |
|---|---|---|
| 定义方式 | 手动创建路由 | 直接定义函数 |
| 调用方式 | fetch('/api/...') | 直接调用函数 |
| 类型安全 | 需要手动同步 | 自动推导 |
| 表单集成 | 需要手动处理 | 原生支持 |
| 渐进增强 | 需要额外代码 | 内置支持 |
| 错误处理 | 手动try-catch | 统一错误边界 |
传统API示例
javascript
// 服务端
app.post('/api/users', async (req, res) => {
const { name, email } = req.body;
const user = await db.users.create({ name, email });
res.json({ success: true, userId: user.id });
});
// 客户端
async function handleSubmit(e) {
e.preventDefault();
const formData = new FormData(e.target);
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: formData.get('name'),
email: formData.get('email'),
}),
});
const data = await response.json();
// 处理响应...
}Server Actions示例
javascript
// 服务端
'use server';
export async function createUser(formData) {
const name = formData.get('name');
const email = formData.get('email');
const user = await db.users.create({ name, email });
return { success: true, userId: user.id };
}
// 客户端
<form action={createUser}>
<input name="name" />
<input name="email" />
<button type="submit">提交</button>
</form>代码量减少了70%,且更加类型安全!
11.2 Server Actions实现
Server Actions的实现涉及编译时转换、运行时引用、网络传输等多个环节。
11.2.1 函数引用的序列化
Server Actions需要在客户端和服务端之间传递函数引用,但函数本身无法序列化。React通过生成唯一标识符来解决这个问题。
编译时转换
打包工具(如Webpack、Turbopack)会扫描"use server"指令,为每个Server Action生成唯一ID:
javascript
// 源代码
'use server';
export async function createUser(data) {
return await db.users.create(data);
}
// 编译后(简化)
export const createUser = registerServerReference(
async function(data) {
return await db.users.create(data);
},
'file://app/actions.js', // 文件路径
'createUser' // 函数名
);registerServerReference源码
javascript
// 文件:packages/react-server-dom-webpack/src/server/ReactFlightServerConfigWebpackBundler.js
// 行号:89-105(React 19.3.0,简化版)
export function registerServerReference(
proxy: any,
reference: string,
encodeFormAction: void | EncodeFormActionCallback,
): any {
// 创建一个代理对象
return Object.defineProperties(proxy, {
$$typeof: { value: SERVER_REFERENCE_TAG },
$$id: { value: reference },
$$bound: { value: null },
bind: { value: bind },
});
}关键属性
- $$typeof:标记为SERVER_REFERENCE_TAG
- $$id:唯一标识符(文件路径 + 函数名)
- $$bound:绑定的参数(用于.bind())
- bind:自定义bind方法,支持参数预绑定
Flight序列化
当Server Component引用Server Action时,Flight协议会序列化函数引用:
F1:{"id":"file://app/actions.js#createUser","name":"createUser","chunks":[]}- F:表示Server Reference(Function)
- id:函数的唯一标识符
- chunks:依赖的客户端模块(通常为空)
11.2.2 客户端调用机制
客户端接收到Server Action引用后,如何调用服务端函数?
callServer实现
javascript
// 文件:packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js
// 行号:45-85(React 19.3.0,简化版)
function callServer(actionId: string, args: any[]) {
// 1. 序列化参数
const body = encodeReply(args);
// 2. 发送POST请求
const response = await fetch('/__action', {
method: 'POST',
headers: {
'Content-Type': 'text/x-component',
'X-Action-Id': actionId,
},
body: body,
});
// 3. 解析Flight响应
const stream = response.body;
return createFromReadableStream(stream);
}调用流程
表单提交流程
当Server Action用作表单action时:
javascript
// 文件:packages/react-dom-bindings/src/client/ReactDOMComponent.js
// 行号:1850-1920(React 19.3.0,简化版)
function setInitialProperties(
domElement: Element,
tag: string,
props: Object,
) {
// ...
if (tag === 'form') {
if (props.action != null) {
// 如果action是Server Action
if (typeof props.action === 'function') {
// 拦截表单提交
domElement.addEventListener('submit', (event) => {
event.preventDefault();
const formData = new FormData(event.target);
// 调用Server Action
props.action(formData);
});
}
}
}
// ...
}11.2.3 参数的编码与传输
Server Actions的参数需要序列化后通过网络传输。
encodeReply实现
javascript
// 文件:packages/react-server-dom-webpack/src/client/ReactFlightReplyClient.js
// 行号:580-650(React 19.3.0,简化版)
export function encodeReply(
value: ReactServerValue,
): string | FormData {
// 1. 创建FormData
const formData = new FormData();
const pendingParts = 0;
// 2. 序列化值
const json = JSON.stringify(value, (key, val) => {
if (val === null) return null;
// 处理特殊类型
if (typeof val === 'object') {
// Promise
if (typeof val.then === 'function') {
const promiseId = pendingParts++;
val.then(
result => resolveModelToJSON(formData, promiseId, result),
error => rejectModelToJSON(formData, promiseId, error)
);
return '$@' + promiseId.toString(16);
}
// File/Blob
if (val instanceof File || val instanceof Blob) {
const fileId = pendingParts++;
formData.append(fileId.toString(), val);
return '$F' + fileId.toString(16);
}
// Date
if (val instanceof Date) {
return '$D' + val.toISOString();
}
}
return val;
});
// 3. 添加到FormData
formData.append('0', json);
return formData;
}支持的数据类型
| 类型 | 编码格式 | 示例 |
|---|---|---|
| 基本类型 | JSON | "hello", 123, true |
| 对象/数组 | JSON | {"name":"Alice"} |
| Date | $D + ISO字符串 | $D2024-01-01T00:00:00.000Z |
| File/Blob | $F + ID | $F1(FormData附件) |
| Promise | $@ + ID | $@2(异步解析) |
| Server Action | $F + 引用ID | $Ffile://app/actions.js#fn |
示例:复杂参数传输
javascript
// 客户端调用
const file = document.querySelector('input[type=file]').files[0];
await uploadFile({
name: 'avatar.png',
file: file,
metadata: {
uploadedAt: new Date(),
userId: 123,
},
});
// 编码后的FormData
// Part 0 (JSON):
{
"name": "avatar.png",
"file": "$F1",
"metadata": {
"uploadedAt": "$D2024-01-01T10:30:00.000Z",
"userId": 123
}
}
// Part 1 (File):
[Binary data of avatar.png]服务端解码
javascript
// 文件:packages/react-server-dom-webpack/src/server/ReactFlightServerConfigWebpackBundler.js
// 行号:250-320(React 19.3.0,简化版)
export async function decodeReply(
body: FormData,
): Promise<any> {
// 1. 读取主JSON
const json = await body.get('0');
// 2. 解析并还原特殊类型
return JSON.parse(json, (key, value) => {
if (typeof value === 'string') {
// File引用
if (value.startsWith('$F')) {
const id = parseInt(value.slice(2), 16);
return body.get(id.toString());
}
// Date
if (value.startsWith('$D')) {
return new Date(value.slice(2));
}
// Promise引用
if (value.startsWith('$@')) {
const id = parseInt(value.slice(2), 16);
return getPromiseForId(id);
}
}
return value;
});
}这样,客户端和服务端就能安全地传输复杂数据结构!
11.3 表单处理
React 19提供了专门的Hooks来处理表单状态和提交状态,使Server Actions与表单的集成更加优雅。
11.3.1 useFormState Hook
useFormState用于管理表单提交后的状态(如成功消息、错误信息)。
基本用法
javascript
'use client';
import { useFormState } from 'react-dom';
import { createUser } from './actions';
export function UserForm() {
const [state, formAction] = useFormState(createUser, { message: '' });
return (
<form action={formAction}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit">创建用户</button>
{state.message && <p>{state.message}</p>}
</form>
);
}javascript
// actions.js
'use server';
export async function createUser(prevState, formData) {
const name = formData.get('name');
const email = formData.get('email');
try {
await db.users.create({ name, email });
return { message: '用户创建成功!' };
} catch (error) {
return { message: '创建失败:' + error.message };
}
}useFormState源码分析
javascript
// 文件:packages/react-dom/src/client/ReactDOMFormActions.js
// 行号:45-120(React 19.3.0,简化版)
export function useFormState<S, P>(
action: (state: S, payload: P) => Promise<S>,
initialState: S,
permalink?: string,
): [S, (payload: P) => void, boolean] {
// 1. 使用useState管理状态
const [state, setState] = useState(initialState);
const [isPending, setIsPending] = useState(false);
// 2. 包装action
const formAction = useCallback(async (payload) => {
setIsPending(true);
try {
// 调用Server Action,传入当前状态
const newState = await action(state, payload);
setState(newState);
} finally {
setIsPending(false);
}
}, [action, state]);
return [state, formAction, isPending];
}关键特性
- 状态持久化:表单提交后状态保留
- prevState参数:Server Action接收上一次的状态
- pending状态:自动跟踪提交中状态
- 渐进增强:JavaScript未加载时也能工作
渐进增强示例
javascript
'use server';
export async function createUser(prevState, formData) {
const name = formData.get('name');
const email = formData.get('email');
// 验证
if (!name || name.length < 2) {
return {
message: '名字至少2个字符',
errors: { name: '名字太短' }
};
}
if (!email || !email.includes('@')) {
return {
message: '请输入有效的邮箱',
errors: { email: '邮箱格式错误' }
};
}
// 创建用户
try {
const user = await db.users.create({ name, email });
// 重定向到用户页面
redirect(`/users/${user.id}`);
} catch (error) {
return {
message: '创建失败',
errors: { _form: error.message }
};
}
}javascript
'use client';
export function UserForm() {
const [state, formAction, isPending] = useFormState(createUser, {
message: '',
errors: {},
});
return (
<form action={formAction}>
<div>
<label>名字</label>
<input name="name" required />
{state.errors?.name && (
<span className="error">{state.errors.name}</span>
)}
</div>
<div>
<label>邮箱</label>
<input name="email" type="email" required />
{state.errors?.email && (
<span className="error">{state.errors.email}</span>
)}
</div>
<button type="submit" disabled={isPending}>
{isPending ? '提交中...' : '创建用户'}
</button>
{state.message && (
<div className="message">{state.message}</div>
)}
</form>
);
}11.3.2 useFormStatus Hook
useFormStatus用于获取父表单的提交状态,常用于提交按钮组件。
基本用法
javascript
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending, data, method, action } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? '提交中...' : '提交'}
</button>
);
}
export function UserForm() {
return (
<form action={createUser}>
<input name="name" />
<input name="email" />
<SubmitButton />
</form>
);
}useFormStatus源码分析
javascript
// 文件:packages/react-dom/src/client/ReactDOMFormActions.js
// 行号:180-220(React 19.3.0,简化版)
export function useFormStatus(): FormStatus {
// 1. 从Context读取表单状态
const status = useContext(FormStatusContext);
if (status === null) {
// 不在表单内部
return {
pending: false,
data: null,
method: null,
action: null,
};
}
return status;
}FormStatusContext的设置
javascript
// 文件:packages/react-dom-bindings/src/client/ReactDOMComponent.js
// 行号:1900-1950(React 19.3.0,简化版)
function handleFormSubmit(event, action) {
event.preventDefault();
const formData = new FormData(event.target);
// 设置pending状态
const formStatus = {
pending: true,
data: formData,
method: event.target.method,
action: action,
};
// 通过Context传递给子组件
startTransition(() => {
FormStatusContext.Provider({
value: formStatus,
children: /* 重新渲染表单 */
});
// 调用Server Action
action(formData).finally(() => {
// 清除pending状态
formStatus.pending = false;
});
});
}返回值说明
| 属性 | 类型 | 说明 |
|---|---|---|
| pending | boolean | 表单是否正在提交 |
| data | FormData | null | 提交的表单数据 |
| method | string | null | 表单method(get/post) |
| action | string | function | null | 表单action |
高级示例:带进度的提交按钮
javascript
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton({ children }) {
const { pending, data } = useFormStatus();
// 根据表单数据显示不同文本
const buttonText = pending
? `正在${data?.get('action') === 'delete' ? '删除' : '保存'}...`
: children;
return (
<button type="submit" disabled={pending} className={pending ? 'loading' : ''}>
{pending && <Spinner />}
{buttonText}
</button>
);
}
function Spinner() {
return <span className="spinner">⏳</span>;
}11.3.3 渐进增强
Server Actions的一大优势是支持渐进增强:即使JavaScript未加载,表单也能正常工作。
工作原理
- 服务端渲染:表单的action属性指向一个真实的HTTP端点
- JavaScript未加载:浏览器执行标准表单提交
- JavaScript加载后:React拦截提交,使用fetch调用Server Action
实现细节
javascript
// 服务端渲染时
<form action="/__action?id=file://app/actions.js#createUser" method="POST">
<input name="name" />
<button type="submit">提交</button>
</form>
// JavaScript加载后,React拦截submit事件
form.addEventListener('submit', (e) => {
e.preventDefault();
const formData = new FormData(e.target);
callServer('file://app/actions.js#createUser', [formData]);
});Next.js中的实现
javascript
// app/actions.js
'use server';
export async function createUser(formData) {
const name = formData.get('name');
await db.users.create({ name });
// 重定向(无论是否有JavaScript都能工作)
redirect('/users');
}javascript
// app/UserForm.jsx
export function UserForm() {
return (
<form action={createUser}>
<input name="name" required />
<button type="submit">创建用户</button>
<noscript>
<p>此表单在没有JavaScript的情况下也能工作!</p>
</noscript>
</form>
);
}渐进增强的好处
- 更快的首次交互:无需等待JavaScript加载
- 更好的可访问性:屏幕阅读器友好
- 更强的鲁棒性:JavaScript加载失败时仍可用
- SEO友好:搜索引擎可以理解表单
11.3.4 示例:Server Actions表单提交
让我们实现一个完整的用户注册表单,展示所有特性。
javascript
// app/actions.js
'use server';
import { z } from 'zod';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
// 验证schema
const userSchema = z.object({
name: z.string().min(2, '名字至少2个字符'),
email: z.string().email('请输入有效的邮箱'),
age: z.number().min(18, '必须年满18岁'),
});
export async function registerUser(prevState, formData) {
// 1. 解析表单数据
const rawData = {
name: formData.get('name'),
email: formData.get('email'),
age: parseInt(formData.get('age')),
};
// 2. 验证
const result = userSchema.safeParse(rawData);
if (!result.success) {
return {
success: false,
message: '验证失败',
errors: result.error.flatten().fieldErrors,
};
}
// 3. 检查邮箱是否已存在
const existingUser = await db.users.findByEmail(result.data.email);
if (existingUser) {
return {
success: false,
message: '该邮箱已被注册',
errors: { email: ['邮箱已存在'] },
};
}
// 4. 创建用户
try {
const user = await db.users.create(result.data);
// 5. 重新验证缓存
revalidatePath('/users');
// 6. 重定向到用户页面
redirect(`/users/${user.id}`);
} catch (error) {
return {
success: false,
message: '创建失败,请稍后重试',
errors: { _form: [error.message] },
};
}
}javascript
// app/RegisterForm.jsx
'use client';
import { useFormState } from 'react-dom';
import { registerUser } from './actions';
import { SubmitButton } from './SubmitButton';
export function RegisterForm() {
const [state, formAction] = useFormState(registerUser, {
success: false,
message: '',
errors: {},
});
return (
<form action={formAction} className="space-y-4">
<div>
<label htmlFor="name" className="block font-medium">
名字
</label>
<input
id="name"
name="name"
type="text"
required
className="mt-1 block w-full rounded border p-2"
aria-describedby={state.errors?.name ? 'name-error' : undefined}
/>
{state.errors?.name && (
<p id="name-error" className="mt-1 text-sm text-red-600">
{state.errors.name[0]}
</p>
)}
</div>
<div>
<label htmlFor="email" className="block font-medium">
邮箱
</label>
<input
id="email"
name="email"
type="email"
required
className="mt-1 block w-full rounded border p-2"
aria-describedby={state.errors?.email ? 'email-error' : undefined}
/>
{state.errors?.email && (
<p id="email-error" className="mt-1 text-sm text-red-600">
{state.errors.email[0]}
</p>
)}
</div>
<div>
<label htmlFor="age" className="block font-medium">
年龄
</label>
<input
id="age"
name="age"
type="number"
required
min="18"
className="mt-1 block w-full rounded border p-2"
aria-describedby={state.errors?.age ? 'age-error' : undefined}
/>
{state.errors?.age && (
<p id="age-error" className="mt-1 text-sm text-red-600">
{state.errors.age[0]}
</p>
)}
</div>
{state.message && !state.success && (
<div className="rounded bg-red-50 p-3 text-red-800">
{state.message}
</div>
)}
<SubmitButton />
</form>
);
}javascript
// app/SubmitButton.jsx
'use client';
import { useFormStatus } from 'react-dom';
export function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending}
className="w-full rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:bg-gray-400"
>
{pending ? (
<>
<span className="inline-block animate-spin mr-2">⏳</span>
注册中...
</>
) : (
'注册'
)}
</button>
);
}这个示例展示了:
- ✅ 表单验证(客户端和服务端)
- ✅ 错误处理和显示
- ✅ 加载状态
- ✅ 渐进增强
- ✅ 可访问性(aria-describedby)
- ✅ 缓存重新验证
- ✅ 成功后重定向
11.4 Hydration水合机制
Hydration(水合)是将服务端渲染的静态HTML与客户端React实例关联的过程,使页面变得可交互。
11.4.1 什么是Hydration
为什么需要Hydration?
服务端渲染(SSR)生成的HTML是静态的,没有事件监听器、没有状态管理。用户看到页面内容,但无法交互(点击按钮没反应)。
Hydration的作用是:
- 在客户端重新执行React组件
- 将React实例与现有DOM节点关联
- 绑定事件监听器
- 恢复组件状态
Hydration vs 客户端渲染
| 阶段 | 用户可见 | 可交互 | 说明 |
|---|---|---|---|
| 服务端HTML到达 | ✅ | ❌ | 用户看到内容,但无法交互 |
| JavaScript加载 | ✅ | ❌ | 下载React和应用代码 |
| Hydration完成 | ✅ | ✅ | 页面完全可交互 |
客户端渲染(CSR)流程
CSR的问题:用户需要等待JavaScript加载和执行才能看到内容。
11.4.2 hydrateRoot源码分析
hydrateRoot是客户端Hydration的入口函数。
基本用法
javascript
// 客户端入口文件
import { hydrateRoot } from 'react-dom/client';
import App from './App';
// 服务端渲染的HTML已经在DOM中
hydrateRoot(document.getElementById('root'), <App />);hydrateRoot源码
javascript
// 文件:packages/react-dom/src/client/ReactDOMRoot.js
// 行号:180-250(React 19.3.0,简化版)
export function hydrateRoot(
container: Element | Document,
initialChildren: ReactNodeList,
options?: HydrateRootOptions,
): RootType {
// 1. 验证容器
if (!isValidContainer(container)) {
throw new Error('Target container is not a DOM element.');
}
// 2. 创建FiberRoot
const root = createContainer(
container,
ConcurrentRoot, // 并发模式
null,
true, // isHydration = true
null,
'',
options?.onRecoverableError || null,
options?.identifierPrefix || null,
options?.onUncaughtError || null,
options?.onCaughtError || null,
options?.onShellError || null,
options?.onShellReady || null,
options?.onAllReady || null,
);
// 3. 标记为Hydration模式
markContainerAsRoot(root.current, container);
// 4. 监听所有支持的事件
listenToAllSupportedEvents(container);
// 5. 开始Hydration
updateContainer(initialChildren, root, null, null);
return root;
}关键参数
- container:服务端渲染的HTML容器
- initialChildren:React组件树(应与服务端渲染的一致)
- options:
- onRecoverableError:可恢复错误回调
- identifierPrefix:ID前缀(用于多个React根)
Hydration标记
javascript
// 文件:packages/react-reconciler/src/ReactFiberRoot.js
// 行号:120-150(React 19.3.0,简化版)
function createContainer(
containerInfo: Container,
tag: RootTag,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
// ...
) {
const root = createFiberRoot(
containerInfo,
tag,
hydrationCallbacks,
isStrictMode,
// ...
);
// 标记为Hydration模式
if (isHydration) {
root.current.mode |= HydrationMode;
}
return root;
}11.4.3 事件绑定恢复
Hydration的核心任务之一是恢复事件监听器。
服务端渲染的HTML
html
<!-- 服务端生成的HTML,没有事件监听器 -->
<button>点击我</button>客户端Hydration后
javascript
// React在Hydration时会:
// 1. 找到对应的DOM节点
// 2. 绑定事件监听器
button.addEventListener('click', handleClick);事件委托机制
React使用事件委托,在根容器上监听所有事件:
javascript
// 文件:packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
// 行号:450-520(React 19.3.0,简化版)
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
// 1. 获取所有支持的事件类型
const allNativeEvents = [
'click', 'input', 'change', 'submit', 'keydown', 'keyup',
'mousedown', 'mouseup', 'touchstart', 'touchend',
// ... 更多事件
];
// 2. 在根容器上监听
allNativeEvents.forEach(domEventName => {
// 捕获阶段
rootContainerElement.addEventListener(
domEventName,
dispatchEvent,
true // useCapture
);
// 冒泡阶段
rootContainerElement.addEventListener(
domEventName,
dispatchEvent,
false
);
});
}事件分发
javascript
// 文件:packages/react-dom-bindings/src/events/ReactDOMEventListener.js
// 行号:180-250(React 19.3.0,简化版)
function dispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
) {
// 1. 找到触发事件的Fiber节点
const targetInst = getClosestInstanceFromNode(nativeEvent.target);
// 2. 收集事件路径上的所有监听器
const listeners = accumulateEventListeners(
targetInst,
domEventName,
nativeEvent.type,
);
// 3. 创建合成事件
const syntheticEvent = createSyntheticEvent(
domEventName,
nativeEvent,
targetInst,
);
// 4. 按顺序执行监听器
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener(syntheticEvent);
if (syntheticEvent.isPropagationStopped()) {
break;
}
}
}Hydration时的事件绑定
javascript
// 文件:packages/react-dom-bindings/src/client/ReactDOMComponent.js
// 行号:1200-1280(React 19.3.0,简化版)
function hydrateInstance(
instance: Instance,
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object,
) {
// 1. 关联Fiber节点与DOM节点
precacheFiberNode(internalInstanceHandle, instance);
// 2. 更新属性(包括事件监听器)
updateFiberProps(instance, props);
// 3. 验证属性是否匹配
const updatePayload = hydrateProperties(
instance,
type,
props,
rootContainerInstance,
hostContext,
);
return updatePayload;
}示例:按钮点击事件的Hydration
javascript
// 服务端渲染
function Button() {
return <button onClick={() => alert('Clicked!')}>点击我</button>;
}
// 生成的HTML
<button>点击我</button>
// 客户端Hydration
// 1. React找到<button>节点
// 2. 创建对应的Fiber节点
// 3. 将onClick处理器存储在Fiber.memoizedProps中
// 4. 根容器已经监听了'click'事件
// 5. 用户点击时,事件冒泡到根容器
// 6. React找到对应的Fiber节点和onClick处理器
// 7. 执行处理器Hydration流程图
11.5 Selective Hydration
Selective Hydration(选择性水合)是React 18引入的优化技术,允许页面部分区域优先Hydration,提升用户体验。
11.5.1 选择性水合的原理
传统Hydration的问题
在React 18之前,Hydration是全有或全无的:
javascript
// 整个应用必须等待所有组件Hydration完成
hydrateRoot(document, <App />);
// 问题:
// 1. 如果某个组件很慢(如大型图表),整个页面都无法交互
// 2. 用户点击按钮,但因为Hydration未完成而无响应
// 3. 浪费资源:用户可能只关心页面的一小部分Selective Hydration的解决方案
使用Suspense边界,React可以:
- 优先Hydration用户正在交互的区域
- 延迟Hydration不重要的区域
- 流式Hydration:边接收HTML边Hydration
javascript
import { Suspense } from 'react';
function App() {
return (
<div>
<Header />
<Suspense fallback={<Spinner />}>
<Comments /> {/* 可以延迟Hydration */}
</Suspense>
<Suspense fallback={<Spinner />}>
<Chart /> {/* 可以延迟Hydration */}
</Suspense>
<Footer />
</div>
);
}工作流程
11.5.2 优先级调度
React使用Lane优先级模型来调度Hydration任务。
Hydration优先级
javascript
// 文件:packages/react-reconciler/src/ReactFiberLane.js
// 行号:80-120(React 19.3.0,简化版)
// Lane优先级(数字越小优先级越高)
export const SyncLane: Lane = 0b0000000000000000000000000000001;
export const InputContinuousLane: Lane = 0b0000000000000000000000000000100;
export const DefaultLane: Lane = 0b0000000000000000000000000010000;
export const IdleLane: Lane = 0b0100000000000000000000000000000;
// Hydration使用的Lane
export const SyncHydrationLane: Lane = 0b0000000000000000000000000000010;
export const DefaultHydrationLane: Lane = 0b0000000000000000000000000100000;优先级提升
当用户与未Hydration的区域交互时,React会提升该区域的优先级:
javascript
// 文件:packages/react-reconciler/src/ReactFiberHydrationContext.js
// 行号:580-650(React 19.3.0,简化版)
function attemptHydrationAtPriority(
fiber: Fiber,
lane: Lane,
) {
// 1. 检查是否已经Hydration
if (fiber.mode & HydrationMode) {
// 2. 提升优先级
const root = enqueueConcurrentRenderForLane(fiber, lane);
// 3. 调度Hydration任务
if (root !== null) {
scheduleUpdateOnFiber(root, fiber, lane);
}
}
}用户交互触发优先Hydration
javascript
// 文件:packages/react-dom-bindings/src/events/ReactDOMEventListener.js
// 行号:320-380(React 19.3.0,简化版)
function dispatchEventForHydration(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
) {
// 1. 找到目标节点
const targetInst = getClosestInstanceFromNode(nativeEvent.target);
// 2. 检查是否需要Hydration
if (targetInst !== null) {
const nearestMounted = getNearestMountedFiber(targetInst);
if (nearestMounted === null) {
// 3. 节点未Hydration,优先处理
attemptHydrationAtPriority(
targetInst,
SyncLane // 最高优先级
);
}
}
// 4. 继续分发事件
dispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent);
}11.5.3 用户交互优先
Selective Hydration的核心优势是响应用户交互。
示例:评论区延迟Hydration
javascript
// App.jsx
import { Suspense } from 'react';
function App() {
return (
<div>
<h1>文章标题</h1>
<article>文章内容...</article>
{/* 评论区可以延迟Hydration */}
<Suspense fallback={<div>加载评论中...</div>}>
<Comments />
</Suspense>
</div>
);
}场景1:用户不关心评论
场景2:用户想查看评论
实现细节
javascript
// 文件:packages/react-reconciler/src/ReactFiberWorkLoop.js
// 行号:1850-1920(React 19.3.0,简化版)
function performConcurrentWorkOnRoot(root, didTimeout) {
// 1. 获取下一个要处理的Lane
const lanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
if (lanes === NoLanes) {
return null;
}
// 2. 检查是否包含Hydration Lane
const includesHydrationLane = includesSomeLane(
lanes,
SyncHydrationLane | DefaultHydrationLane,
);
if (includesHydrationLane) {
// 3. 优先处理Hydration
renderRootSync(root, lanes);
} else {
// 4. 正常并发渲染
renderRootConcurrent(root, lanes);
}
// ...
}性能对比
| 场景 | 传统Hydration | Selective Hydration |
|---|---|---|
| 首次可交互时间 | 等待全部完成(5秒) | 部分立即可用(1秒) |
| 用户点击响应 | 可能无响应 | 立即响应 |
| 资源利用 | 一次性加载全部 | 按需加载 |
| 用户体验 | 较差 | 优秀 |
最佳实践
- 为慢组件添加Suspense
javascript
<Suspense fallback={<Skeleton />}>
<HeavyChart data={data} />
</Suspense>- 为非关键内容添加Suspense
javascript
<Suspense fallback={<div>加载中...</div>}>
<RecommendedArticles />
</Suspense>- 嵌套Suspense实现细粒度控制
javascript
<Suspense fallback={<PageSkeleton />}>
<Header />
<Suspense fallback={<ContentSkeleton />}>
<MainContent />
</Suspense>
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
</Suspense>- 避免过度使用
javascript
// ❌ 不好:每个小组件都包裹Suspense
<Suspense><Button /></Suspense>
<Suspense><Input /></Suspense>
// ✅ 好:为逻辑区域添加Suspense
<Suspense>
<Form>
<Button />
<Input />
</Form>
</Suspense>调试Selective Hydration
javascript
// 在React DevTools中查看Hydration状态
// 1. 打开React DevTools
// 2. 选择Profiler标签
// 3. 开始录制
// 4. 观察Hydration时间线
// 或者使用console.log
if (__DEV__) {
console.log('Hydrating:', fiber.type);
}Selective Hydration是React 18+的重要优化,显著提升了大型应用的用户体验!
11.6 Hydration不匹配处理
Hydration不匹配是指服务端渲染的HTML与客户端React组件生成的结构不一致,这会导致错误和性能问题。
11.6.1 不匹配的检测
React在Hydration时会比较服务端HTML与客户端组件的输出。
常见不匹配场景
- 条件渲染差异
javascript
// ❌ 错误:服务端和客户端渲染不同内容
function Component() {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
return <div>{isClient ? '客户端' : '服务端'}</div>;
}
// 服务端渲染:<div>服务端</div>
// 客户端Hydration:<div>客户端</div> ❌ 不匹配!- 随机值或时间戳
javascript
// ❌ 错误:每次渲染生成不同的值
function Component() {
return <div>随机数:{Math.random()}</div>;
}
// 服务端:<div>随机数:0.123</div>
// 客户端:<div>随机数:0.456</div> ❌ 不匹配!- 浏览器专属API
javascript
// ❌ 错误:服务端没有window对象
function Component() {
return <div>宽度:{window.innerWidth}px</div>;
}
// 服务端:报错或返回undefined
// 客户端:<div>宽度:1920px</div> ❌ 不匹配!不匹配检测源码
javascript
// 文件:packages/react-dom-bindings/src/client/ReactDOMComponent.js
// 行号:1450-1520(React 19.3.0,简化版)
function hydrateTextInstance(
textInstance: TextInstance,
text: string,
internalInstanceHandle: Object,
): boolean {
// 1. 获取DOM节点的文本内容
const textNode: Text = textInstance;
const textContent = textNode.textContent;
// 2. 比较服务端和客户端的文本
if (textContent !== text) {
// 3. 检测到不匹配
if (__DEV__) {
warnForTextDifference(textContent, text);
}
return true; // 需要更新
}
return false; // 匹配,无需更新
}javascript
// 文件:packages/react-dom-bindings/src/client/ReactDOMComponent.js
// 行号:1550-1620(React 19.3.0,简化版)
function hydrateElement(
element: Element,
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object,
): boolean {
// 1. 检查标签类型
if (element.nodeName.toLowerCase() !== type.toLowerCase()) {
if (__DEV__) {
warnForDeletedHydratableElement(element, type);
}
return true; // 不匹配
}
// 2. 检查属性
const updatePayload = hydrateProperties(
element,
type,
props,
rootContainerInstance,
hostContext,
);
// 3. 关联Fiber节点
precacheFiberNode(internalInstanceHandle, element);
updateFiberProps(element, props);
return updatePayload !== null;
}开发环境警告
javascript
// 文件:packages/react-dom-bindings/src/client/ReactDOMComponent.js
// 行号:450-500(React 19.3.0,简化版)
function warnForTextDifference(
serverText: string,
clientText: string,
) {
if (__DEV__) {
console.error(
'Text content did not match. Server: "%s" Client: "%s"',
serverText,
clientText,
);
}
}
function warnForDeletedHydratableElement(
parentNode: Element,
expectedType: string,
) {
if (__DEV__) {
console.error(
'Expected server HTML to contain a matching <%s> in <%s>.',
expectedType,
parentNode.nodeName.toLowerCase(),
);
}
}11.6.2 错误恢复策略
当检测到不匹配时,React会尝试恢复。
恢复策略
- 客户端渲染覆盖:删除服务端HTML,重新渲染
- 部分更新:只更新不匹配的节点
- 抛出错误:严重不匹配时终止Hydration
javascript
// 文件:packages/react-reconciler/src/ReactFiberHydrationContext.js
// 行号:850-920(React 19.3.0,简化版)
function tryToClaimNextHydratableInstance(fiber: Fiber): void {
const nextInstance = nextHydratableInstance;
if (!nextInstance) {
// 1. 没有可Hydration的节点,切换到客户端渲染
fiber.flags |= Placement;
isHydrating = false;
hydrationParentFiber = fiber;
return;
}
const expectedType = getExpectedType(fiber);
const actualType = getActualType(nextInstance);
if (expectedType !== actualType) {
// 2. 类型不匹配,删除服务端节点
deleteHydratableInstance(fiber, nextInstance);
// 3. 标记需要插入新节点
fiber.flags |= Placement;
} else {
// 4. 类型匹配,继续Hydration
fiber.stateNode = nextInstance;
hydrationParentFiber = fiber;
nextHydratableInstance = getNextHydratableSibling(nextInstance);
}
}删除不匹配的节点
javascript
// 文件:packages/react-reconciler/src/ReactFiberHydrationContext.js
// 行号:650-700(React 19.3.0,简化版)
function deleteHydratableInstance(
returnFiber: Fiber,
instance: HydratableInstance,
) {
// 1. 创建删除Fiber
const deletionFiber = createFiberFromHostInstanceForDeletion();
deletionFiber.stateNode = instance;
deletionFiber.return = returnFiber;
deletionFiber.flags = Deletion;
// 2. 添加到副作用链
if (returnFiber.deletions === null) {
returnFiber.deletions = [deletionFiber];
returnFiber.flags |= ChildDeletion;
} else {
returnFiber.deletions.push(deletionFiber);
}
}恢复流程图
11.6.3 suppressHydrationWarning
对于已知的不匹配(如时间戳),可以使用suppressHydrationWarning抑制警告。
基本用法
javascript
function Component() {
return (
<div suppressHydrationWarning>
{new Date().toLocaleString()}
</div>
);
}源码实现
javascript
// 文件:packages/react-dom-bindings/src/client/ReactDOMComponent.js
// 行号:1480-1520(React 19.3.0,简化版)
function hydrateTextInstance(
textInstance: TextInstance,
text: string,
internalInstanceHandle: Object,
): boolean {
const textNode: Text = textInstance;
const textContent = textNode.textContent;
// 检查是否抑制警告
const fiber = internalInstanceHandle;
const shouldSuppressWarning =
fiber.memoizedProps.suppressHydrationWarning === true;
if (textContent !== text) {
// 只在未抑制时发出警告
if (__DEV__ && !shouldSuppressWarning) {
warnForTextDifference(textContent, text);
}
return true;
}
return false;
}使用场景
- 时间戳
javascript
function Clock() {
return (
<time suppressHydrationWarning>
{new Date().toISOString()}
</time>
);
}- 用户特定内容
javascript
function Greeting() {
const [name, setName] = useState('');
useEffect(() => {
setName(localStorage.getItem('userName') || 'Guest');
}, []);
return (
<div suppressHydrationWarning>
欢迎,{name}!
</div>
);
}- 第三方脚本注入的内容
javascript
function AdContainer() {
return (
<div
id="ad-container"
suppressHydrationWarning
>
{/* 广告脚本会在这里注入内容 */}
</div>
);
}注意事项
⚠️ suppressHydrationWarning只抑制直接子节点的警告,不会递归抑制:
javascript
// ✅ 抑制<div>的文本内容警告
<div suppressHydrationWarning>
{Math.random()}
</div>
// ❌ 不会抑制<span>的警告
<div suppressHydrationWarning>
<span>{Math.random()}</span>
</div>
// ✅ 需要在<span>上也添加
<div suppressHydrationWarning>
<span suppressHydrationWarning>{Math.random()}</span>
</div>最佳实践:避免不匹配
- 使用useEffect处理客户端专属逻辑
javascript
// ✅ 好:服务端和客户端初始渲染一致
function Component() {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
if (!isClient) {
return <div>加载中...</div>;
}
return <div>客户端内容</div>;
}- 使用环境检测
javascript
// ✅ 好:检测环境
function Component() {
const width = typeof window !== 'undefined'
? window.innerWidth
: 0;
return <div>宽度:{width}px</div>;
}- 使用固定的初始值
javascript
// ✅ 好:服务端和客户端使用相同的初始值
function Component() {
const [count, setCount] = useState(0); // 固定初始值
return (
<div>
<p>计数:{count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}- 延迟渲染动态内容
javascript
// ✅ 好:动态内容在客户端渲染
function Component() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return (
<div>
<h1>标题</h1>
{mounted && <DynamicContent />}
</div>
);
}调试不匹配
javascript
// 在React DevTools中查看Hydration错误
// 1. 打开Console
// 2. 查找"Hydration"相关警告
// 3. 点击警告查看详细信息
// 或者使用自定义错误处理
function App() {
return (
<ErrorBoundary
onError={(error, errorInfo) => {
if (error.message.includes('Hydration')) {
console.log('Hydration错误:', error, errorInfo);
}
}}
>
<YourApp />
</ErrorBoundary>
);
}正确处理Hydration不匹配是构建可靠SSR应用的关键!
11.7 综合案例:全栈React应用的数据流
让我们通过一个完整的博客应用,理解Server Actions、RSC、Hydration的协作流程。
案例概述
功能需求
- 展示博客文章列表(Server Component)
- 文章详情页(Server Component + Client Component)
- 评论功能(Server Actions + 表单)
- 点赞功能(Server Actions + 乐观更新)
技术栈
- React 19
- Next.js 15 (App Router)
- PostgreSQL数据库
- Prisma ORM
项目结构
app/
├── layout.jsx # 根布局
├── page.jsx # 首页(文章列表)
├── posts/
│ └── [id]/
│ └── page.jsx # 文章详情页
├── actions.js # Server Actions
└── components/
├── PostList.jsx # 文章列表(Server Component)
├── PostDetail.jsx # 文章详情(Server Component)
├── CommentForm.jsx # 评论表单(Client Component)
├── LikeButton.jsx # 点赞按钮(Client Component)
└── CommentList.jsx # 评论列表(Server Component)数据库Schema
prisma
// prisma/schema.prisma
model Post {
id Int @id @default(autoincrement())
title String
content String
author String
likes Int @default(0)
createdAt DateTime @default(now())
comments Comment[]
}
model Comment {
id Int @id @default(autoincrement())
content String
author String
postId Int
post Post @relation(fields: [postId], references: [id])
createdAt DateTime @default(now())
}实现代码
1. Server Actions定义
javascript
// app/actions.js
'use server';
import { revalidatePath } from 'next/cache';
import { prisma } from '@/lib/prisma';
import { z } from 'zod';
// 评论验证schema
const commentSchema = z.object({
content: z.string().min(1, '评论不能为空').max(500, '评论最多500字'),
author: z.string().min(2, '名字至少2个字符'),
postId: z.number(),
});
// 创建评论
export async function createComment(prevState, formData) {
// 1. 解析表单数据
const rawData = {
content: formData.get('content'),
author: formData.get('author'),
postId: parseInt(formData.get('postId')),
};
// 2. 验证
const result = commentSchema.safeParse(rawData);
if (!result.success) {
return {
success: false,
message: '验证失败',
errors: result.error.flatten().fieldErrors,
};
}
// 3. 创建评论
try {
await prisma.comment.create({
data: result.data,
});
// 4. 重新验证缓存
revalidatePath(`/posts/${result.data.postId}`);
return {
success: true,
message: '评论发布成功!',
errors: {},
};
} catch (error) {
return {
success: false,
message: '发布失败,请稍后重试',
errors: { _form: [error.message] },
};
}
}
// 点赞文章
export async function likePost(postId) {
try {
const post = await prisma.post.update({
where: { id: postId },
data: { likes: { increment: 1 } },
});
// 重新验证缓存
revalidatePath(`/posts/${postId}`);
return { success: true, likes: post.likes };
} catch (error) {
return { success: false, error: error.message };
}
}
// 删除评论
export async function deleteComment(commentId, postId) {
try {
await prisma.comment.delete({
where: { id: commentId },
});
revalidatePath(`/posts/${postId}`);
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
}2. 首页 - 文章列表(Server Component)
javascript
// app/page.jsx
import { prisma } from '@/lib/prisma';
import Link from 'next/link';
export default async function HomePage() {
// 在服务端直接查询数据库
const posts = await prisma.post.findMany({
orderBy: { createdAt: 'desc' },
take: 10,
});
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-4xl font-bold mb-8">博客文章</h1>
<div className="space-y-6">
{posts.map(post => (
<article key={post.id} className="border rounded-lg p-6">
<Link href={`/posts/${post.id}`}>
<h2 className="text-2xl font-semibold hover:text-blue-600">
{post.title}
</h2>
</Link>
<p className="text-gray-600 mt-2">
{post.content.substring(0, 150)}...
</p>
<div className="flex items-center gap-4 mt-4 text-sm text-gray-500">
<span>作者:{post.author}</span>
<span>❤️ {post.likes}</span>
<span>{post.createdAt.toLocaleDateString()}</span>
</div>
</article>
))}
</div>
</div>
);
}3. 文章详情页(Server Component + Client Component)
javascript
// app/posts/[id]/page.jsx
import { Suspense } from 'react';
import { prisma } from '@/lib/prisma';
import { notFound } from 'next/navigation';
import { LikeButton } from '@/components/LikeButton';
import { CommentForm } from '@/components/CommentForm';
import { CommentList } from '@/components/CommentList';
export default async function PostPage({ params }) {
const postId = parseInt(params.id);
// 并行获取文章和评论
const [post, comments] = await Promise.all([
prisma.post.findUnique({ where: { id: postId } }),
prisma.comment.findMany({
where: { postId },
orderBy: { createdAt: 'desc' },
}),
]);
if (!post) {
notFound();
}
return (
<div className="container mx-auto px-4 py-8 max-w-3xl">
{/* 文章内容 - Server Component */}
<article>
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<div className="flex items-center gap-4 mb-8 text-gray-600">
<span>作者:{post.author}</span>
<span>{post.createdAt.toLocaleDateString()}</span>
</div>
<div className="prose max-w-none mb-8">
{post.content}
</div>
{/* 点赞按钮 - Client Component */}
<LikeButton postId={post.id} initialLikes={post.likes} />
</article>
<hr className="my-12" />
{/* 评论区 */}
<section>
<h2 className="text-2xl font-bold mb-6">
评论 ({comments.length})
</h2>
{/* 评论表单 - Client Component */}
<CommentForm postId={post.id} />
{/* 评论列表 - Server Component */}
<Suspense fallback={<div>加载评论中...</div>}>
<CommentList comments={comments} postId={post.id} />
</Suspense>
</section>
</div>
);
}4. 点赞按钮(Client Component + 乐观更新)
javascript
// app/components/LikeButton.jsx
'use client';
import { useState, useTransition } from 'react';
import { likePost } from '@/app/actions';
export function LikeButton({ postId, initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
const [isPending, startTransition] = useTransition();
const handleLike = () => {
// 乐观更新:立即更新UI
setLikes(prev => prev + 1);
// 调用Server Action
startTransition(async () => {
const result = await likePost(postId);
if (!result.success) {
// 失败时回滚
setLikes(prev => prev - 1);
alert('点赞失败:' + result.error);
} else {
// 使用服务端返回的真实值
setLikes(result.likes);
}
});
};
return (
<button
onClick={handleLike}
disabled={isPending}
className="flex items-center gap-2 px-4 py-2 bg-red-100 hover:bg-red-200 rounded-lg disabled:opacity-50"
>
<span className="text-2xl">❤️</span>
<span className="font-semibold">{likes}</span>
{isPending && <span className="text-sm">(更新中...)</span>}
</button>
);
}5. 评论表单(Client Component)
javascript
// app/components/CommentForm.jsx
'use client';
import { useFormState, useFormStatus } from 'react-dom';
import { createComment } from '@/app/actions';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
>
{pending ? '发布中...' : '发布评论'}
</button>
);
}
export function CommentForm({ postId }) {
const [state, formAction] = useFormState(createComment, {
success: false,
message: '',
errors: {},
});
return (
<form action={formAction} className="mb-8 space-y-4">
<input type="hidden" name="postId" value={postId} />
<div>
<label htmlFor="author" className="block font-medium mb-2">
名字
</label>
<input
id="author"
name="author"
type="text"
required
className="w-full px-4 py-2 border rounded-lg"
aria-describedby={state.errors?.author ? 'author-error' : undefined}
/>
{state.errors?.author && (
<p id="author-error" className="mt-1 text-sm text-red-600">
{state.errors.author[0]}
</p>
)}
</div>
<div>
<label htmlFor="content" className="block font-medium mb-2">
评论内容
</label>
<textarea
id="content"
name="content"
required
rows={4}
className="w-full px-4 py-2 border rounded-lg"
aria-describedby={state.errors?.content ? 'content-error' : undefined}
/>
{state.errors?.content && (
<p id="content-error" className="mt-1 text-sm text-red-600">
{state.errors.content[0]}
</p>
)}
</div>
{state.message && (
<div className={`p-3 rounded-lg ${
state.success ? 'bg-green-50 text-green-800' : 'bg-red-50 text-red-800'
}`}>
{state.message}
</div>
)}
<SubmitButton />
</form>
);
}6. 评论列表(Server Component)
javascript
// app/components/CommentList.jsx
import { deleteComment } from '@/app/actions';
import { DeleteButton } from './DeleteButton';
export function CommentList({ comments, postId }) {
if (comments.length === 0) {
return (
<p className="text-gray-500 text-center py-8">
还没有评论,来发表第一条吧!
</p>
);
}
return (
<div className="space-y-4 mt-6">
{comments.map(comment => (
<div key={comment.id} className="border rounded-lg p-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<span className="font-semibold">{comment.author}</span>
<span className="text-sm text-gray-500">
{comment.createdAt.toLocaleDateString()}
</span>
</div>
<p className="text-gray-700">{comment.content}</p>
</div>
{/* 删除按钮 - Client Component */}
<DeleteButton
commentId={comment.id}
postId={postId}
onDelete={deleteComment}
/>
</div>
</div>
))}
</div>
);
}7. 删除按钮(Client Component)
javascript
// app/components/DeleteButton.jsx
'use client';
import { useTransition } from 'react';
export function DeleteButton({ commentId, postId, onDelete }) {
const [isPending, startTransition] = useTransition();
const handleDelete = () => {
if (!confirm('确定要删除这条评论吗?')) {
return;
}
startTransition(async () => {
const result = await onDelete(commentId, postId);
if (!result.success) {
alert('删除失败:' + result.error);
}
});
};
return (
<button
onClick={handleDelete}
disabled={isPending}
className="text-red-600 hover:text-red-800 disabled:opacity-50"
>
{isPending ? '删除中...' : '删除'}
</button>
);
}完整数据流图解
关键技术点总结
Server Components
- 文章列表、文章详情、评论列表都是Server Component
- 直接访问数据库,无需API层
- 减少客户端JavaScript体积
Client Components
- 点赞按钮、评论表单、删除按钮需要交互
- 使用"use client"指令标记
- 通过Flight协议传递引用
Server Actions
- createComment、likePost、deleteComment
- 类型安全的服务端函数调用
- 自动处理序列化和错误
Hydration
- 服务端渲染HTML,客户端Hydration
- Selective Hydration:评论区可延迟加载
- 事件绑定自动恢复
乐观更新
- 点赞立即更新UI
- 失败时回滚
- 提升用户体验
缓存重新验证
- revalidatePath刷新缓存
- 确保数据一致性
- 自动更新相关页面
这个案例展示了React 19全栈架构的完整数据流,从服务端渲染到客户端交互,再到数据更新和缓存管理!
本章小结
本章深入讲解了React 19的Server Actions和Hydration机制:
Server Actions:革命性的服务端函数调用机制,消除了传统API层,提供类型安全的全栈开发体验。
表单处理:useFormState和useFormStatus Hooks简化了表单状态管理,支持渐进增强。
Hydration:将服务端HTML与客户端React实例关联,恢复事件监听器和组件状态。
Selective Hydration:优先Hydration用户交互的区域,显著提升首次可交互时间。
不匹配处理:检测和恢复服务端与客户端的渲染差异,确保应用稳定性。
综合案例:通过博客应用展示了完整的全栈React数据流。
思考题
Server Actions与传统REST API相比有哪些优势和劣势?在什么场景下应该使用传统API?
为什么Selective Hydration能够提升用户体验?它是如何检测用户交互并调整优先级的?
如何避免Hydration不匹配?如果必须在服务端和客户端渲染不同内容,应该如何处理?
乐观更新在什么情况下会失败?如何设计一个健壮的乐观更新机制?
在大型应用中,如何合理划分Server Components和Client Components的边界?
下一章预告
在第12章中,我们将学习React的提交阶段(commit phase),理解React如何将变更应用到DOM、Effect的执行时机、以及三个子阶段的详细流程。