React Server Actions 入门与进阶:从表单处理到数据突变的最佳实践

By | 2026年7月30日

前言

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 本身不提供加载状态,但我们可以使用 useFormStatususeFormState 钩子来增强体验。


"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 主要用于突变。
  • 缓存失效:使用 revalidatePathrevalidateTag 确保数据一致性。

实战:构建一个完整的评论系统

结合上述知识,我们来构建一个带乐观更新的评论表单。

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 进行单元测试。希望本文能帮助你在实际项目中用好这个特性。

延伸阅读