Skip to content

Server Actions

React 19 的数据变更新范式,简化全栈开发

1. 什么是 Server Actions?

1.1 核心概念

Server Actions = 可以在客户端调用的服务端函数,无需创建 API 路由。

jsx
// 传统方式:需要 API 路由
// pages/api/create-post.js
export default async function handler(req, res) {
    const { title } = req.body;
    await db.posts.create({ title });
    res.json({ success: true });
}

// 组件中调用
fetch('/api/create-post', { method: 'POST', body: JSON.stringify({ title }) });
jsx
// Server Actions:直接定义服务端函数
async function createPost(formData) {
    'use server';
    const title = formData.get('title');
    await db.posts.create({ title });
}

// 直接在表单中使用
<form action={createPost}>
    <input name="title" />
    <button>Create</button>
</form>

1.2 定义方式

方式一:内联定义

jsx
function Page() {
    async function handleSubmit(formData) {
        'use server';  // 标记为 Server Action
        // 这里的代码只在服务端运行
        await db.insert(formData);
    }
    
    return <form action={handleSubmit}>...</form>;
}

方式二:独立文件

jsx
// actions.ts
'use server';

export async function createPost(formData: FormData) {
    const title = formData.get('title') as string;
    await db.posts.create({ title });
}

export async function deletePost(id: string) {
    await db.posts.delete(id);
}
jsx
// component.tsx
import { createPost } from './actions';

export function Form() {
    return (
        <form action={createPost}>
            <input name="title" />
            <button>Create</button>
        </form>
    );
}

2. 表单处理

2.1 基础表单

jsx
// actions.ts
'use server';

export async function signup(formData: FormData) {
    const email = formData.get('email') as string;
    const password = formData.get('password') as string;
    
    // 验证
    if (!email || !password) {
        return { error: 'Missing fields' };
    }
    
    // 创建用户
    await db.users.create({ email, password: hash(password) });
    
    // 重定向
    redirect('/dashboard');
}
jsx
// form.tsx
import { signup } from './actions';

export function SignupForm() {
    return (
        <form action={signup}>
            <input name="email" type="email" required />
            <input name="password" type="password" required />
            <button type="submit">Sign Up</button>
        </form>
    );
}

2.2 配合 useActionState

jsx
'use client';

import { useActionState } from 'react';
import { signup } from './actions';

export function SignupForm() {
    const [state, action, isPending] = useActionState(signup, { error: null });
    
    return (
        <form action={action}>
            <input name="email" type="email" />
            <input name="password" type="password" />
            
            {state.error && <p className="error">{state.error}</p>}
            
            <button disabled={isPending}>
                {isPending ? 'Signing up...' : 'Sign Up'}
            </button>
        </form>
    );
}

2.3 表单验证

jsx
// actions.ts
'use server';

import { z } from 'zod';

const schema = z.object({
    email: z.string().email('Invalid email'),
    password: z.string().min(8, 'Password must be at least 8 characters'),
});

export async function signup(prevState: any, formData: FormData) {
    const result = schema.safeParse({
        email: formData.get('email'),
        password: formData.get('password'),
    });
    
    if (!result.success) {
        return {
            errors: result.error.flatten().fieldErrors,
        };
    }
    
    // 验证通过,创建用户
    await db.users.create(result.data);
    redirect('/dashboard');
}

3. 非表单场景

3.1 事件处理

jsx
'use client';

import { likePost } from './actions';

export function LikeButton({ postId }: { postId: string }) {
    return (
        <button onClick={() => likePost(postId)}>
            Like
        </button>
    );
}
jsx
// actions.ts
'use server';

export async function likePost(postId: string) {
    await db.posts.update(postId, {
        likes: { increment: 1 }
    });
    revalidatePath('/posts');
}

3.2 配合 useTransition

jsx
'use client';

import { useTransition } from 'react';
import { deletePost } from './actions';

export function DeleteButton({ postId }: { postId: string }) {
    const [isPending, startTransition] = useTransition();
    
    const handleDelete = () => {
        startTransition(async () => {
            await deletePost(postId);
        });
    };
    
    return (
        <button onClick={handleDelete} disabled={isPending}>
            {isPending ? 'Deleting...' : 'Delete'}
        </button>
    );
}

3.3 乐观更新

jsx
'use client';

import { useOptimistic } from 'react';
import { toggleLike } from './actions';

export function LikeButton({ postId, liked, count }) {
    const [optimisticLiked, setOptimisticLiked] = useOptimistic(liked);
    const [optimisticCount, setOptimisticCount] = useOptimistic(count);
    
    const handleClick = async () => {
        // 乐观更新
        setOptimisticLiked(!optimisticLiked);
        setOptimisticCount(optimisticLiked ? count - 1 : count + 1);
        
        // 实际请求
        await toggleLike(postId);
    };
    
    return (
        <button onClick={handleClick}>
            {optimisticLiked ? '❤️' : '🤍'} {optimisticCount}
        </button>
    );
}

4. 缓存重验证

4.1 revalidatePath

jsx
'use server';

import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
    await db.posts.create({...});
    
    // 重验证特定路径
    revalidatePath('/posts');
    
    // 重验证动态路径
    revalidatePath('/posts/[id]', 'page');
    
    // 重验证整个布局
    revalidatePath('/posts', 'layout');
}

4.2 revalidateTag

jsx
// 数据获取时打标签
async function getPosts() {
    const res = await fetch('https://api.example.com/posts', {
        next: { tags: ['posts'] }
    });
    return res.json();
}

// Server Action 中重验证标签
'use server';

import { revalidateTag } from 'next/cache';

export async function createPost(formData: FormData) {
    await db.posts.create({...});
    revalidateTag('posts');  // 所有 'posts' 标签的缓存失效
}

5. 错误处理

5.1 返回错误状态

jsx
'use server';

export async function createPost(prevState: any, formData: FormData) {
    try {
        const title = formData.get('title') as string;
        
        if (!title) {
            return { error: 'Title is required', success: false };
        }
        
        await db.posts.create({ title });
        return { error: null, success: true };
        
    } catch (e) {
        return { error: 'Failed to create post', success: false };
    }
}

5.2 抛出错误(配合 Error Boundary)

jsx
'use server';

export async function deletePost(id: string) {
    const post = await db.posts.find(id);
    
    if (!post) {
        throw new Error('Post not found');  // 会被 Error Boundary 捕获
    }
    
    await db.posts.delete(id);
}
jsx
// error.tsx (Next.js)
'use client';

export default function Error({ error, reset }) {
    return (
        <div>
            <h2>Something went wrong!</h2>
            <p>{error.message}</p>
            <button onClick={reset}>Try again</button>
        </div>
    );
}

6. 安全性

6.1 身份验证

jsx
'use server';

import { auth } from '@/lib/auth';

export async function createPost(formData: FormData) {
    // 验证登录状态
    const session = await auth();
    if (!session) {
        throw new Error('Unauthorized');
    }
    
    await db.posts.create({
        title: formData.get('title'),
        authorId: session.user.id,
    });
}

6.2 权限检查

jsx
'use server';

export async function deletePost(id: string) {
    const session = await auth();
    const post = await db.posts.find(id);
    
    // 只能删除自己的文章
    if (post.authorId !== session.user.id) {
        throw new Error('Forbidden');
    }
    
    await db.posts.delete(id);
}

6.3 输入验证

jsx
'use server';

import { z } from 'zod';

const PostSchema = z.object({
    title: z.string().min(1).max(100),
    content: z.string().min(1).max(10000),
});

export async function createPost(formData: FormData) {
    // 永远验证输入!Server Actions 可以被直接调用
    const validated = PostSchema.parse({
        title: formData.get('title'),
        content: formData.get('content'),
    });
    
    await db.posts.create(validated);
}

7. 高级模式

7.1 带参数的 Action

jsx
// 方式一:bind
'use client';

import { deletePost } from './actions';

export function DeleteButton({ postId }) {
    const deletePostWithId = deletePost.bind(null, postId);
    
    return (
        <form action={deletePostWithId}>
            <button>Delete</button>
        </form>
    );
}

// actions.ts
'use server';
export async function deletePost(postId: string, formData: FormData) {
    await db.posts.delete(postId);
}
jsx
// 方式二:hidden input
export function DeleteButton({ postId }) {
    return (
        <form action={deletePost}>
            <input type="hidden" name="postId" value={postId} />
            <button>Delete</button>
        </form>
    );
}

7.2 返回数据

jsx
'use server';

export async function searchPosts(query: string) {
    const posts = await db.posts.search(query);
    return posts;  // 可以返回数据给客户端
}

// 客户端
'use client';

async function handleSearch(query: string) {
    const results = await searchPosts(query);
    setResults(results);
}

7.3 文件上传

jsx
'use server';

export async function uploadImage(formData: FormData) {
    const file = formData.get('file') as File;
    
    // 验证文件
    if (!file || file.size === 0) {
        return { error: 'No file uploaded' };
    }
    
    if (file.size > 5 * 1024 * 1024) {
        return { error: 'File too large' };
    }
    
    // 上传到存储服务
    const bytes = await file.arrayBuffer();
    const buffer = Buffer.from(bytes);
    
    const url = await uploadToStorage(buffer, file.name);
    return { url };
}

8. 面试高频问题

Q1: Server Actions 和 API Routes 的区别?

对比项API RoutesServer Actions
定义方式独立文件内联或独立文件
调用方式fetch()直接调用函数
类型安全手动定义自动推断
表单集成手动处理原生 form action
代码量

Q2: Server Actions 安全吗?

安全考虑

  1. Server Actions 暴露为 HTTP 端点,可以被直接调用
  2. 必须验证身份和权限
  3. 必须验证输入数据
  4. 不要信任客户端传来的任何数据

Q3: 什么时候用 Server Actions vs API Routes?

Server Actions 适合

  • 表单提交
  • 简单的数据变更
  • 与 React 组件紧密集成

API Routes 适合

  • 需要被外部服务调用
  • Webhook 处理
  • 复杂的 RESTful API

Q4: 如何处理 Server Actions 的 loading 状态?

jsx
// 方式一:useActionState
const [state, action, isPending] = useActionState(serverAction, initial);

// 方式二:useTransition
const [isPending, startTransition] = useTransition();
startTransition(() => serverAction());

// 方式三:useFormStatus (在表单子组件中)
const { pending } = useFormStatus();

Q5: Server Actions 可以返回什么?

可以返回任何可序列化的值:

  • 原始类型(string, number, boolean)
  • 对象和数组
  • Date(会被序列化)
  • 不能返回函数、类实例、Symbol 等

前端面试知识库