5.1 Server Actions 是什么
Server Actions 是 Next.js(基于 React 19 规范)提供的一种机制,允许在服务端执行异步函数,可以直接从 Client Component 调用,无需手动创建 API 路由。Next.js 会自动为每个 Server Action 生成一个唯一的 POST 端点,客户端调用时通过这个端点通信。
Server Actions 彻底改变了全栈开发的工作流:以往需要写 API Route → 前端 fetch → 处理错误的三步流程,现在只需在服务端函数上标注 'use server',前端像调用普通函数一样调用即可。安全性由框架保证,CSRF 防护内置其中。
内置安全性Server Actions 内置了 CSRF 防护——Next.js 会验证请求的 Origin 头与当前域名匹配,且只接受 POST 请求。此外,Action 的 ID 是随机生成的哈希值,不会暴露函数名或实现细节。
5.2 定义与调用 Server Actions
有两种方式定义 Server Actions:
// 方式1:在专用文件中定义(推荐,复用性好)
// app/actions/post.ts
'use server'
import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
// 数据验证
if (!title || title.length < 3) {
return { error: '标题至少3个字符' }
}
// 直接操作数据库
await db.post.create({
data: { title, content }
})
// 使相关路径的缓存失效
revalidatePath('/blog')
// 重定向到列表页
redirect('/blog')
}
TS
// 方式2:在 Server Component 内联定义
// app/new-post/page.tsx
export default function NewPostPage() {
// 内联定义,需要标注 'use server'
async function createPost(formData: FormData) {
'use server'
const title = formData.get('title')
await db.post.create({ data: { title } })
}
return (
{/* form 的 action 直接传入函数 */}
<form action={createPost}>
<input name="title" />
<button type="submit">创建</button>
</form>
)
}
TSX
5.3 表单与 useFormStatus
React 19 引入了 useFormStatus Hook,可以在表单的子组件中获取当前表单的提交状态。这解决了以往需要手动管理 loading 状态的问题——当表单正在提交 Server Action 时,pending 自动变为 true。
注意:useFormStatus 必须在 <form> 的子组件内使用,不能在 form 本身的组件中使用。
'use client'
import { useFormStatus } from 'react-dom'
// 提交按钮组件(必须是 form 的子组件)
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button
type="submit"
disabled={pending}
aria-disabled={pending}
>
{pending ? '提交中...' : '提交'}
</button>
)
}
// 表单组件(可以是 Server Component)
import { createPost } from '@/app/actions/post'
export default function CreatePostForm() {
return (
<form action={createPost}>
<input name="title" placeholder="标题" required />
<textarea name="content" placeholder="内容" />
<SubmitButton /> {/* 子组件,可以读取 form 状态 */}
</form>
)
}
TSX
5.4 useActionState — 处理错误与反馈
useActionState(React 19 正式 API,Next.js 14 中叫 useFormState)允许 Server Action 返回状态,并在客户端组件中读取该状态。这是处理表单验证错误、成功消息的标准模式。
// app/actions/post.ts
'use server'
export type ActionState = {
error?: string
success?: boolean
}
export async function createPost(
prevState: ActionState, // useActionState 注入
formData: FormData
): Promise<ActionState> {
const title = formData.get('title') as string
if (!title) {
return { error: '标题不能为空' }
}
try {
await db.post.create({ data: { title } })
revalidatePath('/blog')
return { success: true }
} catch (e) {
return { error: '创建失败,请稍后重试' }
}
}
TS
'use client'
import { useActionState } from 'react'
import { useFormStatus } from 'react-dom'
import { createPost, type ActionState } from '@/app/actions/post'
export function CreatePostForm() {
const [state, action, pending] = useActionState<ActionState, FormData>(
createPost,
{} // 初始状态
)
return (
<form action={action}>
{state.error && (
<p className="error">{state.error}</p>
)}
{state.success && (
<p className="success">创建成功!</p>
)}
<input name="title" disabled={pending} />
<button type="submit" disabled={pending}>
{pending ? '提交中...' : '创建文章'}
</button>
</form>
)
}
TSX
5.5 useOptimistic — 乐观更新
乐观更新(Optimistic Update)是一种 UI 模式:在 Server Action 完成之前,先在 UI 上预先显示预期的结果。如果 Action 成功,UI 保持不变;如果失败,UI 回滚到原始状态。这极大提升了用户感知的响应速度,是现代应用(如 Twitter 点赞、评论)的标配体验。
React 19 的 useOptimistic Hook 内置了这个模式的状态管理。
'use client'
import { useOptimistic, useTransition } from 'react'
import { toggleLike } from '@/app/actions/like'
interface LikeButtonProps {
postId: string
initialLiked: boolean
likeCount: number
}
export function LikeButton({ postId, initialLiked, likeCount }: LikeButtonProps) {
const [isPending, startTransition] = useTransition()
// useOptimistic:第一个参数是真实状态,第二个是更新函数
const [optimisticState, setOptimistic] = useOptimistic(
{ liked: initialLiked, count: likeCount },
(current, newLiked: boolean) => ({
liked: newLiked,
count: newLiked ? current.count + 1 : current.count - 1
})
)
async function handleClick() {
startTransition(async () => {
// 立即更新 UI(乐观)
setOptimistic(!optimisticState.liked)
// 实际调用服务端(可能有延迟)
await toggleLike(postId)
// 成功后 optimisticState 被真实状态替换
// 失败后 React 会自动回滚乐观状态
})
}
return (
<button onClick={handleClick} disabled={isPending}>
{optimisticState.liked ? '❤️' : '🤍'} {optimisticState.count}
</button>
)
}
TSX
5.6 Zod 表单数据验证
生产应用中应使用 Zod 进行严格的数据验证。Zod 提供类型安全的 Schema 定义,既可以在 Server Action 中验证输入数据,也可以共用于前端实时验证。
import { z } from 'zod'
// 定义验证 Schema
const CreatePostSchema = z.object({
title: z.string()
.min(3, '标题至少3个字符')
.max(100, '标题最多100个字符'),
content: z.string().min(10, '内容至少10个字符'),
published: z.boolean().default(false),
})
export type CreatePostInput = z.infer<typeof CreatePostSchema>
export async function createPost(prevState: ActionState, formData: FormData) {
'use server'
const raw = {
title: formData.get('title'),
content: formData.get('content'),
published: formData.get('published') === 'true',
}
const result = CreatePostSchema.safeParse(raw)
if (!result.success) {
// 返回字段级别的错误信息
return {
error: result.error.flatten().fieldErrors,
}
}
await db.post.create({ data: result.data })
revalidatePath('/blog')
return { success: true }
}
TS
永远不要信任客户端输入即使在前端做了验证,Server Action 中必须重新验证所有输入数据。恶意用户可以绕过前端直接向 Action 端点发送请求。Zod 的 safeParse 不会抛出异常,适合服务端使用。