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