name: server-actions description: 当用户询问关于"Server Actions"、“Next.js中的表单处理”、“mutations”、“useFormState”、“useFormStatus”、“revalidatePath”、“revalidateTag”,或需要关于Next.js App Router中数据变异和表单提交的指导时,应使用此技能。 version: 1.0.0
Next.js 服务器操作
概述
服务器操作是在服务器上执行的异步函数。它们可以从客户端和服务器组件调用,用于数据变异、表单提交和其他服务器端操作。
定义服务器操作
在服务器组件中
在异步函数内部使用 'use server' 指令:
// app/page.tsx (服务器组件)
export default function Page() {
async function createPost(formData: FormData) {
'use server'
const title = formData.get('title') as string
await db.post.create({ data: { title } })
}
return (
<form action={createPost}>
<input name="title" />
<button type="submit">创建</button>
</form>
)
}
在单独文件中
在整个文件上标记 'use server':
// app/actions.ts
'use server'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
await db.post.create({ data: { title } })
}
export async function deletePost(id: string) {
await db.post.delete({ where: { id } })
}
表单处理
基本表单
// app/actions.ts
'use server'
export async function submitContact(formData: FormData) {
const name = formData.get('name') as string
const email = formData.get('email') as string
const message = formData.get('message') as string
await db.contact.create({
data: { name, email, message }
})
}
// app/contact/page.tsx
import { submitContact } from '@/app/actions'
export default function ContactPage() {
return (
<form action={submitContact}>
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required />
<button type="submit">发送</button>
</form>
)
}
使用验证(Zod)
// app/actions.ts
'use server'
import { z } from 'zod'
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
})
export async function signup(formData: FormData) {
const parsed = schema.safeParse({
email: formData.get('email'),
password: formData.get('password'),
})
if (!parsed.success) {
return { error: parsed.error.flatten() }
}
await createUser(parsed.data)
return { success: true }
}
useFormState 钩子
处理表单状态和错误:
// app/signup/page.tsx
'use client'
import { useFormState } from 'react-dom'
import { signup } from '@/app/actions'
const initialState = {
error: null,
success: false,
}
export default function SignupPage() {
const [state, formAction] = useFormState(signup, initialState)
return (
<form action={formAction}>
<input name="email" type="email" />
<input name="password" type="password" />
{state.error && (
<p className="text-red-500">{state.error}</p>
)}
<button type="submit">注册</button>
</form>
)
}
// app/actions.ts
'use server'
export async function signup(prevState: any, formData: FormData) {
const email = formData.get('email') as string
if (!email.includes('@')) {
return { error: '无效邮箱', success: false }
}
await createUser({ email })
return { error: null, success: true }
}
useFormStatus 钩子
显示提交期间的加载状态:
// 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>
)
}
// 在表单中使用
import { SubmitButton } from '@/components/submit-button'
export default function Form() {
return (
<form action={submitAction}>
<input name="title" />
<SubmitButton />
</form>
)
}
重新验证
revalidatePath
重新验证特定路径:
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
await db.post.create({ data: { ... } })
// 重新验证帖子列表页面
revalidatePath('/posts')
// 重新验证动态路由
revalidatePath('/posts/[slug]', 'page')
// 重新验证 /posts 下的所有路径
revalidatePath('/posts', 'layout')
}
revalidateTag
通过缓存标签重新验证:
// 使用标签获取
const posts = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }
})
// 服务器操作
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost(formData: FormData) {
await db.post.create({ data: { ... } })
revalidateTag('posts')
}
操作后重定向
'use server'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
const post = await db.post.create({ data: { ... } })
// 重定向到新帖子
redirect(`/posts/${post.slug}`)
}
乐观更新
在操作完成时立即更新UI:
'use client'
import { useOptimistic } from 'react'
import { addTodo } from '@/app/actions'
export function TodoList({ todos }: { todos: Todo[] }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo: string) => [
...state,
{ id: 'temp', title: newTodo, completed: false }
]
)
async function handleSubmit(formData: FormData) {
const title = formData.get('title') as string
addOptimisticTodo(title) // 立即更新UI
await addTodo(formData) // 服务器操作
}
return (
<>
<form action={handleSubmit}>
<input name="title" />
<button>添加</button>
</form>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</>
)
}
非表单使用
以编程方式调用服务器操作:
'use client'
import { deletePost } from '@/app/actions'
export function DeleteButton({ id }: { id: string }) {
return (
<button onClick={() => deletePost(id)}>
删除
</button>
)
}
错误处理
'use server'
export async function createPost(formData: FormData) {
try {
await db.post.create({ data: { ... } })
return { success: true }
} catch (error) {
if (error instanceof PrismaClientKnownRequestError) {
if (error.code === 'P2002') {
return { error: '已存在具有此标题的帖子' }
}
}
return { error: '创建帖子失败' }
}
}
安全考虑
- 始终验证输入 - 不要信任客户端数据
- 检查身份验证 - 验证用户是否授权
- 使用CSRF保护 - 服务器操作内置保护
- 消毒输出 - 防止XSS攻击
'use server'
import { auth } from '@/lib/auth'
export async function deletePost(id: string) {
const session = await auth()
if (!session) {
throw new Error('未经授权')
}
const post = await db.post.findUnique({ where: { id } })
if (post.authorId !== session.user.id) {
throw new Error('禁止访问')
}
await db.post.delete({ where: { id } })
}
资源
有关详细模式,请参阅:
references/form-handling.md- 高级表单模式references/revalidation.md- 缓存重新验证策略examples/mutation-patterns.md- 完整变异示例