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 |
| Webhook | API 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 | 乐观更新提升体验 |
| 权限检查 | 敏感操作必须验证 |
