Server Actions 是什么

async function createPost(formData: FormData) {
    'use server';                           // ★ 服务端函数
    const title = formData.get('title');
    await db.posts.create({ title });
}

export default function NewPost() {
    return (
        <form action={createPost}>
            <input name="title" required />
            <button type="submit">Create</button>
        </form>
    );
}

提交表单 → createPost服务端跑 → 不用单独写 API endpoint。

标记方式

1. 函数内 directive

async function action(data: FormData) {
    'use server';
    // 这个函数在服务端跑
}

2. 文件顶部 directive

// src/actions/posts.ts
'use server';

export async function createPost(formData: FormData) { ... }
export async function deletePost(id: string) { ... }

文件里所有 export 函数都是 Server Action

完整例子

// src/actions/posts.ts
'use server';

import { redirect } from 'next/navigation';
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';

export async function createPost(formData: FormData) {
    const title = formData.get('title') as string;
    const body = formData.get('body') as string;

    if (!title || !body) {
        throw new Error('Missing fields');
    }

    const post = await db.posts.create({ title, body });

    revalidatePath('/blog');                     // 刷新列表
    redirect(`/blog/${post.slug}`);              // 跳转
}
// src/app/blog/new/page.tsx
import { createPost } from '@/actions/posts';

export default function NewPostPage() {
    return (
        <form action={createPost} className="space-y-4">
            <input name="title" required className="border p-2 w-full" />
            <textarea name="body" required className="border p-2 w-full h-32" />
            <button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded">
                Create
            </button>
        </form>
    );
}

没有 fetch,没有 API endpoint,没有 useState——直接表单到数据库。

处理加载状态:useFormStatus

'use client';
import { useFormStatus } from 'react-dom';

function SubmitButton() {
    const { pending } = useFormStatus();
    return (
        <button type="submit" disabled={pending}>
            {pending ? 'Creating...' : 'Create'}
        </button>
    );
}

// 用:
<form action={createPost}>
    <input name="title" />
    <SubmitButton />
</form>

useFormStatus 必须在 form 子组件里——所以 SubmitButton 单独提出。

处理返回值 / 错误:useActionState

'use client';
import { useActionState } from 'react';
import { createPost } from '@/actions/posts';

export default function NewPostForm() {
    const [state, action, pending] = useActionState(createPost, null);

    return (
        <form action={action}>
            <input name="title" />
            {state?.error && <p className="text-red-600">{state.error}</p>}
            <button type="submit" disabled={pending}>
                {pending ? 'Saving...' : 'Save'}
            </button>
        </form>
    );
}
// action 改成接 state 参数
'use server';
export async function createPost(prevState: any, formData: FormData) {
    try {
        // ...
        return { ok: true };
    } catch (err) {
        return { error: err.message };
    }
}

不用表单也能用 Server Action

'use client';
import { deletePost } from '@/actions/posts';

<button onClick={() => deletePost(post.id)}>Delete</button>

非表单调用就是普通函数——只是它跑在服务端

鉴权(必须做)

Server Actions 可以被任何人调用——永远在 action 里验权限

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

export async function deletePost(id: string) {
    const session = await auth();
    if (!session) throw new Error('Unauthorized');

    const post = await db.posts.findUnique({ where: { id } });
    if (post.authorId !== session.userId) throw new Error('Forbidden');

    await db.posts.delete({ where: { id } });
    revalidatePath('/blog');
}

表单校验

'use server';
import { z } from 'zod';

const PostSchema = z.object({
    title: z.string().min(1).max(200),
    body: z.string().min(10),
});

export async function createPost(formData: FormData) {
    const parsed = PostSchema.safeParse({
        title: formData.get('title'),
        body: formData.get('body'),
    });

    if (!parsed.success) {
        return { errors: parsed.error.flatten() };
    }

    await db.posts.create({ data: parsed.data });
}

zod 是事实标准的 schema 校验库。

优势 vs 传统 API

维度 Server Action 传统 API endpoint
文件数 1 个函数 1 个 API + 1 个组件
类型安全 TS 自动跨边界 手动同步
网络请求 浏览器原生表单 / fetch(自动) 手写 fetch
学习曲线

何时用 API endpoint

  • 给第三方调(webhook 接收方)
  • 给浏览器之外的客户端(移动 App / curl)
  • 流式响应
// src/app/api/posts/route.ts
export async function POST(request: Request) {
    const data = await request.json();
    // ...
    return Response.json({ ok: true });
}

  • Server Actions 永远在服务端跑——window / document 不可用
  • 大文件上传不要用 FormData——用 chunked upload / S3 直传
  • Action 抛错会跳 error.tsx——要友好提示用 return error
  • 别忘 revalidatePath / revalidateTag——否则缓存还是老数据

下一篇:SEO metadata。