前言
React Server Actions 是 React 19 引入的一个重磅特性,它允许我们在客户端组件中直接调用服务端函数,而无需手动编写 API 路由。这极大简化了数据突变(Mutation)的流程,尤其是表单提交、数据更新等操作。然而,许多开发者对 Server Actions 的理解停留在表面,容易陷入“过度使用”或“错误使用”的陷阱。本文将带你从零开始,深入理解 Server Actions 的原理、使用场景以及进阶技巧。
什么是 Server Actions?
简单来说,Server Actions 是定义在服务端的异步函数,可以在客户端组件中通过 "use server" 指令调用。它们直接运行在服务端,可以安全地访问数据库、文件系统等资源,而无需暴露 API 端点。
基本用法
首先,创建一个 Server Action 文件(例如 app/actions.ts):
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
export async function createUser(formData: FormData) {
const name = formData.get("name");
const email = formData.get("email");
// 模拟数据库操作
console.log(`Creating user: ${name}, ${email}`);
// 重新验证路径以刷新数据
revalidatePath("/users");
return { success: true };
}
然后在客户端组件中使用它:
// app/page.tsx
"use client";
import { createUser } from "./actions";
export default function Home() {
return (
<form action={createUser}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<button type="submit">Create User</button>
</form>
);
}
注意:表单的 action 属性可以直接接收 Server Action 函数,React 会自动处理提交并显示加载状态。
进阶用法与最佳实践
1. 处理加载状态与错误
Server Actions 本身不提供加载状态,但我们可以使用 useFormStatus 和 useFormState 钩子来增强体验。
"use client";
import { useFormStatus } from "react-dom";
import { createUser } from "./actions";
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Creating..." : "Create User"}
</button>
);
}
export default function Home() {
return (
<form action={createUser}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<SubmitButton />
</form>
);
}
对于错误处理,可以使用 useFormState:
"use client";
import { useFormState } from "react-dom";
import { createUser } from "./actions";
const initialState = { message: null };
function Form() {
const [state, formAction] = useFormState(createUser, initialState);
return (
<form action={formAction}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<button type="submit">Create User</button>
{state?.message && <p>{state.message}</p>}
</form>
);
}
对应的 Server Action 需要返回一个状态对象:
"use server";
export async function createUser(prevState: any, formData: FormData) {
try {
// 模拟数据库操作可能失败
const name = formData.get("name");
if (!name) throw new Error("Name is required");
return { message: "User created successfully" };
} catch (error) {
return { message: error.message };
}
}
2. 参数验证与类型安全
Server Actions 接收的参数可以是 FormData 或普通对象。推荐使用 zod 进行验证:
"use server";
import { z } from "zod";
const schema = z.object({
name: z.string().min(2),
email: z.string().email(),
});
export async function createUser(prevState: any, formData: FormData) {
const validatedFields = schema.safeParse({
name: formData.get("name"),
email: formData.get("email"),
});
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
};
}
// 安全地使用 validatedFields.data
console.log(validatedFields.data);
return { message: "Success" };
}
3. 与第三方库集成(如 Prisma)
Server Actions 可以直接调用 Prisma:
"use server";
import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache";
export async function addTodo(formData: FormData) {
const title = formData.get("title") as string;
await prisma.todo.create({
data: { title },
});
revalidatePath("/todos");
}
注意:确保 Prisma 客户端在服务端初始化,不要在客户端导入。
4. 渐进增强与乐观更新
Server Actions 天然支持渐进增强(即使 JavaScript 未加载,表单也能提交)。对于更好的用户体验,可以使用乐观更新:
"use client";
import { useOptimistic } from "react";
import { addTodo } from "./actions";
export default function TodoList({ todos }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, newTodo]
);
async function formAction(formData) {
const title = formData.get("title");
addOptimisticTodo({ id: Date.now(), title, completed: false });
await addTodo(formData);
}
return (
<form action={formAction}>
<input name="title" required />
<button type="submit">Add</button>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</form>
);
}
5. 避免常见陷阱
- 不要在客户端组件中导入服务端模块:Server Actions 文件应只包含
"use server"函数,不要导入数据库客户端等。 - 注意安全:Server Actions 是公开的,不要信任用户输入,始终进行验证。
- 不要滥用:对于简单的数据获取,仍然推荐使用服务端组件或 API 路由。Server Actions 主要用于突变。
- 缓存失效:使用
revalidatePath或revalidateTag确保数据一致性。
实战:构建一个完整的评论系统
结合上述知识,我们来构建一个带乐观更新的评论表单。
1. 定义 Server Action
// app/actions.ts
"use server";
import { z } from "zod";
import { revalidatePath } from "next/cache";
const commentSchema = z.object({
postId: z.string(),
content: z.string().min(1).max(500),
});
export async function addComment(prevState: any, formData: FormData) {
const validated = commentSchema.safeParse({
postId: formData.get("postId"),
content: formData.get("content"),
});
if (!validated.success) {
return { errors: validated.error.flatten().fieldErrors };
}
// 模拟数据库操作
const comment = {
id: Date.now(),
postId: validated.data.postId,
content: validated.data.content,
createdAt: new Date().toISOString(),
};
console.log("Comment added:", comment);
revalidatePath(`/posts/${validated.data.postId}`);
return { comment };
}
2. 客户端组件
// app/components/CommentForm.tsx
"use client";
import { useFormState, useOptimistic } from "react-dom";
import { addComment } from "../actions";
const initialState = { errors: null, comment: null };
export default function CommentForm({ postId }) {
const [state, formAction] = useFormState(addComment, initialState);
const [optimisticComments, setOptimisticComments] = useOptimistic([], (state, newComment) => [...state, newComment]);
async function handleSubmit(formData) {
const newComment = {
id: Date.now(),
postId,
content: formData.get("content"),
createdAt: new Date().toISOString(),
};
setOptimisticComments(newComment);
await formAction(formData);
}
return (
<div>
<form action={handleSubmit}>
<input type="hidden" name="postId" value={postId} />
<textarea name="content" placeholder="Write a comment..." required />
<button type="submit">Submit</button>
{state?.errors?.content && <p style={{color: 'red'}}>{state.errors.content}</p>}
</form>
<ul>
{optimisticComments.map(comment => (
<li key={comment.id}>{comment.content} (optimistic)</li>
))}
</ul>
</div>
);
}
总结
Server Actions 是 React 生态中数据突变的一把利器,它简化了前后端交互,同时保持了服务端的安全性和性能。本文从基础用法到进阶实践,涵盖了表单处理、验证、乐观更新等核心场景。记住,Server Actions 最适合用于表单提交和数据更新,而对于数据获取,服务端组件仍然是更好的选择。
下一步,你可以探索 Server Actions 与流式传输(Streaming)的结合,或者研究如何对 Server Actions 进行单元测试。希望本文能帮助你在实际项目中用好这个特性。