为什么需要 Server Actions?
在传统的 React 应用中,前后端交互通常需要手动创建 API 路由、编写 fetch 请求、处理加载和错误状态。这不仅繁琐,还容易导致代码重复和类型不安全。React Server Actions(RSC Actions)允许你在组件中直接定义异步函数,在服务器端运行,并通过网络调用,从而简化数据变更逻辑。
Server Actions 是 React 18/19 中与 Server Components 配合的重要特性,它让你能像调用本地函数一样调用服务器函数,同时保持渐进增强和类型安全。本文将从零开始,带你掌握 Server Actions 的核心用法,并深入实战场景。
环境准备
首先,确保你使用支持 Server Actions 的框架,例如 Next.js 14+(App Router)或 Remix。本文以 Next.js 14 为例。
npx create-next-app@latest my-app --typescript --app
cd my-app
在 next.config.js 中启用实验性功能(若使用 Next.js 14,默认已启用):
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: true,
},
};
module.exports = nextConfig;
第一步:创建第一个 Server Action
Server Action 本质上是一个异步函数,使用 'use server' 指令标记。它可以在服务端组件中定义,也可以在单独的文件中定义。
在 app/actions.ts 中:
'use server';
export async function createTodo(formData: FormData) {
const title = formData.get('title') as string;
if (!title || title.trim().length === 0) {
throw new Error('Title is required');
}
// 模拟数据库插入
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log('Todo created:', title);
return { success: true };
}
在服务端组件中直接使用:
// app/page.tsx
import { createTodo } from './actions';
export default function Home() {
return (
<form action={createTodo}>
<input type="text" name="title" placeholder="Enter todo" />
<button type="submit">Add</button>
</form>
);
}
当表单提交时,React 会自动调用 createTodo,并将表单数据作为 FormData 传入。无需手动 fetch,也无需定义 API 路由。
💡 注意:Server Action 必须是一个异步函数,且只能在服务端组件或客户端组件中通过 action 属性或 startTransition 调用。
第二步:处理错误与加载状态
在客户端组件中,你可以使用 useFormStatus 获取表单提交状态,或使用 useActionState(React 19)管理返回值和错误。
'use client';
import { useFormStatus } from 'react-dom';
import { createTodo } from './actions';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Adding...' : 'Add'}
</button>
);
}
export function TodoForm() {
return (
<form action={createTodo}>
<input type="text" name="title" required />
<SubmitButton />
</form>
);
}
对于错误处理,可以结合 useActionState(以前叫 useFormState):
'use client';
import { useActionState } from 'react';
import { createTodo } from './actions';
const initialState = { error: null };
export function TodoForm() {
const [state, formAction] = useActionState(async (prevState, formData) => {
try {
await createTodo(formData);
return { error: null };
} catch (e) {
return { error: e.message };
}
}, initialState);
return (
<form action={formAction}>
<input type="text" name="title" required />
{state.error && <p style={{ color: 'red' }}>{state.error}</p>}
<button type="submit">Add</button>
</form>
);
}
第三步:进阶——乐观更新与重新验证
Server Actions 常与 useOptimistic 和 revalidatePath 搭配,实现即时 UI 反馈和数据刷新。
假设我们有一个待办列表,从数据库获取数据。在 Server Action 中插入后,调用 revalidatePath('/') 让页面重新获取最新数据。
'use server';
import { revalidatePath } from 'next/cache';
export async function addTodo(formData: FormData) {
const title = formData.get('title') as string;
// 插入数据库...
revalidatePath('/');
}
在客户端组件中,使用 useOptimistic 实现乐观更新:
'use client';
import { useOptimistic } from 'react';
import { addTodo } from './actions';
export function TodoList({ todos }: { todos: string[] }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo: string) => [...state, newTodo]
);
async function formAction(formData: FormData) {
const title = formData.get('title') as string;
addOptimisticTodo(title);
await addTodo(formData);
}
return (
<div>
<form action={formAction}>
<input type="text" name="title" required />
<button type="submit">Add</button>
</form>
<ul>
{optimisticTodos.map((todo, i) => (
<li key={i}>{todo}</li>
))}
</ul>
</div>
);
}
这样,用户提交后立即看到新待办,而无需等待服务器响应。
第四步:安全最佳实践
Server Actions 直接暴露在网络上,必须进行安全防护:
- 始终验证输入:使用 Zod 等库进行模式验证。
- 认证与授权:在 Action 内部检查用户会话,避免未授权访问。
- 限制暴露的 Action:避免在客户端组件中导入未经验证的 Action。
示例:使用 Zod 验证并检查认证。
'use server';
import { z } from 'zod';
import { auth } from '@/auth'; // 假设的认证工具
const schema = z.object({
title: z.string().min(1).max(100),
});
export async function createTodo(formData: FormData) {
const session = await auth();
if (!session) throw new Error('Unauthorized');
const parsed = schema.safeParse({ title: formData.get('title') });
if (!parsed.success) throw new Error('Invalid input');
// 安全地使用 parsed.data.title
}
常见坑与性能优化
- 不要在客户端组件中直接定义 Server Action:必须使用
'use server'指令,且函数必须是异步的。 - 避免在 Action 中执行长时间任务:Server Action 默认有超时限制,长时间任务应使用后台作业。
- 合理使用 revalidatePath 和 revalidateTag:过度重新验证会导致性能下降,建议按需使用。
- 错误处理:抛出错误会触发最近的错误边界,建议返回结构化结果而非直接抛出。
总结
React Server Actions 极大地简化了全栈 React 应用的数据变更逻辑,通过本文的入门与进阶示例,你应该已经掌握了从表单提交到乐观更新的完整流程。记住:始终验证输入、检查认证、合理使用重新验证,才能构建出既高效又安全的应用。
下一步,你可以探索 Server Actions 与 Server Components 的深度集成、使用 useActionState 管理复杂表单状态,以及结合数据库 ORM 实现完整 CRUD。