Next.js Server Actions #

一、Server Actions概述 #

1.1 什么是Server Actions #

Server Actions是在服务端执行的函数,可以直接在组件中调用:

tsx
'use server'

export async function createPost(formData: FormData) {
    const title = formData.get('title')
    await db.post.create({ data: { title } })
}

1.2 基本用法 #

tsx
import { createPost } from './actions'

export default function NewPostForm() {
    return (
        <form action={createPost}>
            <input name="title" required />
            <button type="submit">发布</button>
        </form>
    )
}

1.3 声明方式 #

文件级声明

tsx
'use server'

export async function myAction() {
}

函数级声明

tsx
export default function Component() {
    async function myAction() {
        'use server'
    }
    
    return <button onClick={myAction}>Click</button>
}

二、表单处理 #

2.1 基本表单 #

tsx
'use server'

import { revalidatePath } from 'next/cache'

export async function createPost(formData: FormData) {
    const title = formData.get('title') as string
    const content = formData.get('content') as string
    
    await db.post.create({
        data: { title, content },
    })
    
    revalidatePath('/blog')
}
tsx
import { createPost } from './actions'

export default function NewPostForm() {
    return (
        <form action={createPost}>
            <div>
                <label>标题</label>
                <input name="title" required />
            </div>
            <div>
                <label>内容</label>
                <textarea name="content" required />
            </div>
            <button type="submit">发布</button>
        </form>
    )
}

2.2 带参数的Action #

tsx
'use server'

import { revalidatePath } from 'next/cache'

export async function deletePost(postId: string) {
    await db.post.delete({
        where: { id: postId },
    })
    
    revalidatePath('/blog')
}
tsx
'use client'

import { deletePost } from './actions'

export default function DeleteButton({ postId }: { postId: string }) {
    return (
        <button onClick={() => deletePost(postId)}>
            删除
        </button>
    )
}

2.3 表单状态 #

tsx
'use client'

import { useActionState } from 'react'
import { createPost } from './actions'

export default function NewPostForm() {
    const [state, formAction, isPending] = useActionState(createPost, null)
    
    return (
        <form action={formAction}>
            <input name="title" required />
            <textarea name="content" required />
            <button type="submit" disabled={isPending}>
                {isPending ? '发布中...' : '发布'}
            </button>
            {state?.error && <p className="text-red-500">{state.error}</p>}
            {state?.success && <p className="text-green-500">发布成功!</p>}
        </form>
    )
}
tsx
'use server'

import { revalidatePath } from 'next/cache'

interface FormState {
    error?: string
    success?: boolean
}

export async function createPost(prevState: FormState, formData: FormData): Promise<FormState> {
    const title = formData.get('title') as string
    const content = formData.get('content') as string
    
    if (!title || title.length < 3) {
        return { error: '标题至少3个字符' }
    }
    
    try {
        await db.post.create({
            data: { title, content },
        })
        
        revalidatePath('/blog')
        return { success: true }
    } catch {
        return { error: '发布失败' }
    }
}

2.4 表单状态 #

tsx
'use client'

import { useFormStatus } from 'react-dom'

function SubmitButton() {
    const { pending } = useFormStatus()
    
    return (
        <button type="submit" disabled={pending}>
            {pending ? '提交中...' : '提交'}
        </button>
    )
}

export default function Form() {
    return (
        <form action={submitAction}>
            <input name="field" />
            <SubmitButton />
        </form>
    )
}

三、数据验证 #

3.1 Zod验证 #

tsx
'use server'

import { z } from 'zod'
import { revalidatePath } from 'next/cache'

const postSchema = z.object({
    title: z.string().min(3, '标题至少3个字符').max(100, '标题过长'),
    content: z.string().min(10, '内容至少10个字符'),
})

export async function createPost(prevState: any, formData: FormData) {
    const result = postSchema.safeParse({
        title: formData.get('title'),
        content: formData.get('content'),
    })
    
    if (!result.success) {
        return { errors: result.error.flatten().fieldErrors }
    }
    
    await db.post.create({ data: result.data })
    revalidatePath('/blog')
    
    return { success: true }
}

3.2 显示验证错误 #

tsx
'use client'

import { useActionState } from 'react'
import { createPost } from './actions'

export default function NewPostForm() {
    const [state, formAction] = useActionState(createPost, null)
    
    return (
        <form action={formAction}>
            <div>
                <label>标题</label>
                <input name="title" />
                {state?.errors?.title && (
                    <p className="text-red-500">{state.errors.title[0]}</p>
                )}
            </div>
            <div>
                <label>内容</label>
                <textarea name="content" />
                {state?.errors?.content && (
                    <p className="text-red-500">{state.errors.content[0]}</p>
                )}
            </div>
            <button type="submit">发布</button>
        </form>
    )
}

四、乐观更新 #

4.1 useOptimistic #

tsx
'use client'

import { experimental_useOptimistic as useOptimistic } from 'react'
import { addTodo } from './actions'

interface Todo {
    id: string
    text: string
    completed: boolean
}

export default function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
    const [optimisticTodos, addOptimisticTodo] = useOptimistic(
        initialTodos,
        (state, newTodo: Todo) => [...state, newTodo]
    )
    
    async function handleAdd(formData: FormData) {
        const text = formData.get('text') as string
        const tempId = Date.now().toString()
        
        addOptimisticTodo({ id: tempId, text, completed: false })
        await addTodo(text)
    }
    
    return (
        <div>
            <form action={handleAdd}>
                <input name="text" required />
                <button type="submit">添加</button>
            </form>
            <ul>
                {optimisticTodos.map(todo => (
                    <li key={todo.id}>{todo.text}</li>
                ))}
            </ul>
        </div>
    )
}

4.2 点赞示例 #

tsx
'use client'

import { experimental_useOptimistic as useOptimistic } from 'react'
import { likePost } from './actions'

export default function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
    const [optimisticLikes, addLike] = useOptimistic(
        initialLikes,
        (state) => state + 1
    )
    
    return (
        <button
            onClick={async () => {
                addLike(null)
                await likePost(postId)
            }}
        >
            ❤️ {optimisticLikes}
        </button>
    )
}

五、重新验证 #

5.1 revalidatePath #

tsx
'use server'

import { revalidatePath } from 'next/cache'

export async function createPost(data: PostData) {
    await db.post.create({ data })
    
    revalidatePath('/blog')
    revalidatePath('/blog/[slug]', 'page')
}

5.2 revalidateTag #

tsx
'use server'

import { revalidateTag } from 'next/cache'

export async function createPost(data: PostData) {
    await db.post.create({ data })
    
    revalidateTag('posts')
}

5.3 redirect #

tsx
'use server'

import { redirect } from 'next/navigation'

export async function createPost(formData: FormData) {
    const post = await db.post.create({
        data: {
            title: formData.get('title') as string,
            content: formData.get('content') as string,
        },
    })
    
    redirect(`/blog/${post.slug}`)
}

六、认证与权限 #

6.1 检查认证 #

tsx
'use server'

import { auth } from '@/auth'
import { redirect } from 'next/navigation'

export async function createPost(formData: FormData) {
    const session = await auth()
    
    if (!session) {
        throw new Error('未授权')
    }
    
    await db.post.create({
        data: {
            title: formData.get('title') as string,
            authorId: session.user.id,
        },
    })
}

6.2 角色检查 #

tsx
'use server'

import { auth } from '@/auth'

export async function deleteUser(userId: string) {
    const session = await auth()
    
    if (session?.user?.role !== 'admin') {
        throw new Error('权限不足')
    }
    
    await db.user.delete({ where: { id: userId } })
}

七、最佳实践 #

7.1 组织结构 #

text
app/
├── actions/
│   ├── posts.ts
│   ├── users.ts
│   └── comments.ts
├── blog/
│   └── page.tsx

7.2 类型安全 #

tsx
interface PostData {
    title: string
    content: string
}

export async function createPost(data: PostData) {
    await db.post.create({ data })
}

7.3 错误处理 #

tsx
'use server'

export async function createPost(formData: FormData) {
    try {
        await db.post.create({
            data: {
                title: formData.get('title') as string,
            },
        })
        
        revalidatePath('/blog')
        return { success: true }
    } catch (error) {
        console.error('Error:', error)
        return { error: '创建失败' }
    }
}

八、总结 #

Server Actions要点:

要点 说明
声明 ‘use server’
表单处理 action属性
状态管理 useActionState
乐观更新 useOptimistic
重新验证 revalidatePath/Tag
重定向 redirect

下一步,让我们学习优化!

最后更新:2026-03-28