⚠️ Alpha内测版本警告:此为早期内部构建版本,尚不完整且可能存在错误,欢迎大家提Issue反馈问题或建议
Skip to content

2.3.3 Server Actions 最佳实践

一句话破题

Server Actions 让你可以直接在客户端调用服务器函数,无需手动创建 API 路由——但要用好它,需要掌握表单处理、错误处理和乐观更新的最佳实践。

基础用法

定义 Server Action

typescript
// app/actions.ts
'use server'

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('/posts')
}

在表单中使用

typescript
// app/posts/new/page.tsx
import { createPost } from '@/app/actions'

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

表单验证

使用 Zod 验证

typescript
// app/actions.ts
'use server'

import { z } from 'zod'

const PostSchema = z.object({
  title: z.string().min(1, '标题不能为空').max(100),
  content: z.string().min(10, '内容至少 10 个字符'),
})

export async function createPost(formData: FormData) {
  const validatedFields = PostSchema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
  })
  
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }
  
  await db.post.create({ data: validatedFields.data })
  revalidatePath('/posts')
  redirect('/posts')
}

显示验证错误

typescript
// components/post-form.tsx
'use client'

import { useFormState } from 'react-dom'
import { createPost } from '@/app/actions'

export function PostForm() {
  const [state, formAction] = useFormState(createPost, { errors: {} })
  
  return (
    <form action={formAction}>
      <div>
        <input name="title" />
        {state.errors?.title && (
          <p className="text-red-500">{state.errors.title}</p>
        )}
      </div>
      <div>
        <textarea name="content" />
        {state.errors?.content && (
          <p className="text-red-500">{state.errors.content}</p>
        )}
      </div>
      <SubmitButton />
    </form>
  )
}

提交状态处理

typescript
// components/submit-button.tsx
'use client'

import { useFormStatus } from 'react-dom'

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

乐观更新

typescript
// components/like-button.tsx
'use client'

import { useOptimistic } from 'react'
import { likePost } from '@/app/actions'

export function LikeButton({ postId, likes }: { postId: string; likes: number }) {
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    likes,
    (state, _) => state + 1
  )
  
  async function handleLike() {
    addOptimisticLike(null)  // 立即更新 UI
    await likePost(postId)    // 实际请求
  }
  
  return (
    <form action={handleLike}>
      <button type="submit">
        ❤️ {optimisticLikes}
      </button>
    </form>
  )
}

错误处理

Server Action 中抛出错误

typescript
// app/actions.ts
'use server'

export async function deletePost(postId: string) {
  const post = await db.post.findUnique({ where: { id: postId } })
  
  if (!post) {
    throw new Error('文章不存在')
  }
  
  if (post.authorId !== getCurrentUserId()) {
    throw new Error('无权删除此文章')
  }
  
  await db.post.delete({ where: { id: postId } })
  revalidatePath('/posts')
}

error.tsx 捕获错误

typescript
// app/posts/error.tsx
'use client'

export default function Error({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  return (
    <div>
      <h2>出错了:{error.message}</h2>
      <button onClick={reset}>重试</button>
    </div>
  )
}

Server Actions vs API Routes

场景推荐方案
表单提交Server Actions
数据变更Server Actions
第三方调用API Routes
WebhookAPI Routes
复杂查询Server Actions

觉知:常见问题

1. 忘记 'use server'

typescript
// ❌ 没有 'use server',不是 Server Action
export async function createPost(formData) {
  // 这会在客户端执行!
}

// ✅ 添加 'use server'
'use server'
export async function createPost(formData) {
  // 在服务器执行
}

2. 敏感操作不验证

typescript
// ❌ 没有权限检查
export async function deletePost(id) {
  await db.post.delete({ where: { id } })
}

// ✅ 添加权限检查
export async function deletePost(id) {
  const user = await getCurrentUser()
  const post = await db.post.findUnique({ where: { id } })
  
  if (post.authorId !== user.id) {
    throw new Error('无权操作')
  }
  
  await db.post.delete({ where: { id } })
}

本节小结

最佳实践说明
Zod 验证始终验证输入数据
useFormState处理表单状态和错误
useFormStatus显示提交状态
useOptimistic乐观更新提升体验
权限检查敏感操作必须验证