Next.js数据变更与重新验证 #
一、数据变更概述 #
1.1 变更方式 #
| 方式 | 说明 | 适用场景 |
|---|---|---|
| Server Actions | 服务端函数 | 表单提交、数据操作 |
| API Routes | REST API | 外部调用、复杂逻辑 |
| 客户端fetch | 客户端请求 | 简单交互 |
1.2 重新验证方式 #
| 方式 | 说明 |
|---|---|
| 基于时间 | revalidate配置 |
| 按需验证 | revalidatePath/revalidateTag |
| 手动刷新 | router.refresh() |
二、Server Actions #
2.1 基本用法 #
tsx
'use server'
export async function createPost(formData: FormData) {
const title = formData.get('title')
const content = formData.get('content')
await db.post.create({
data: { title, content },
})
}
2.2 在组件中使用 #
tsx
import { createPost } from './actions'
export default function NewPostForm() {
return (
<form action={createPost}>
<input name="title" required />
<textarea name="content" required />
<button type="submit">发布</button>
</form>
)
}
2.3 带参数的Action #
tsx
'use server'
export async function deletePost(postId: string) {
await db.post.delete({
where: { id: postId },
})
revalidatePath('/blog')
}
tsx
import { deletePost } from './actions'
export default function DeleteButton({ postId }: { postId: string }) {
return (
<button onClick={() => deletePost(postId)}>
删除
</button>
)
}
2.4 使用useActionState #
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>}
</form>
)
}
2.5 使用useFormStatus #
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 revalidatePath #
tsx
'use server'
import { revalidatePath } from 'next/cache'
export async function updatePost(slug: string, data: PostData) {
await db.post.update({
where: { slug },
data,
})
revalidatePath('/blog')
revalidatePath(`/blog/${slug}`)
}
3.2 revalidateTag #
tsx
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost(data: PostData) {
await db.post.create({ data })
revalidateTag('posts')
}
数据获取时设置标签:
tsx
export default async function PostList() {
const posts = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] },
}).then(res => res.json())
return <PostList posts={posts} />
}
3.3 重新验证范围 #
| 方法 | 范围 | 说明 |
|---|---|---|
| revalidatePath(‘/’) | 所有页面 | 清除所有缓存 |
| revalidatePath(‘/blog’) | 特定路径 | 清除该路径缓存 |
| revalidateTag(‘posts’) | 特定标签 | 清除带标签的请求 |
3.4 条件重新验证 #
tsx
'use server'
export async function togglePostPublished(postId: string) {
const post = await db.post.findUnique({
where: { id: postId },
})
await db.post.update({
where: { id: postId },
data: { published: !post.published },
})
if (post.published) {
revalidatePath(`/blog/${post.slug}`)
}
}
四、乐观更新 #
4.1 useOptimistic #
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, addOptimisticLike] = useOptimistic(
initialLikes,
(state, amount: number) => state + amount
)
const handleLike = async () => {
addOptimisticLike(1)
await likePost(postId)
}
return (
<button onClick={handleLike}>
❤️ {optimisticLikes}
</button>
)
}
4.2 列表乐观更新 #
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]
)
const handleAdd = async (text: string) => {
const tempId = Date.now().toString()
addOptimisticTodo({ id: tempId, text, completed: false })
await addTodo(text)
}
return (
<div>
<form action={(formData) => handleAdd(formData.get('text') as string)}>
<input name="text" />
<button type="submit">添加</button>
</form>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</div>
)
}
五、表单处理 #
5.1 基本表单 #
tsx
'use server'
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
const post = await db.post.create({
data: { title, content },
})
revalidatePath('/blog')
redirect(`/blog/${post.slug}`)
}
tsx
export default function NewPostPage() {
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>
)
}
5.2 表单验证 #
tsx
'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 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 }
}
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}>
<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" disabled={isPending}>
发布
</button>
</form>
)
}
5.3 文件上传 #
tsx
'use server'
export async function uploadFile(formData: FormData) {
const file = formData.get('file') as File
if (!file) {
return { error: '请选择文件' }
}
const bytes = await file.arrayBuffer()
const buffer = Buffer.from(bytes)
const filename = `${Date.now()}-${file.name}`
await writeFile(`public/uploads/${filename}`, buffer)
return { success: true, filename }
}
tsx
'use client'
import { useState } from 'react'
import { uploadFile } from './actions'
export default function UploadForm() {
const [status, setStatus] = useState<'idle' | 'uploading' | 'success' | 'error'>('idle')
const handleSubmit = async (formData: FormData) => {
setStatus('uploading')
const result = await uploadFile(formData)
setStatus(result.success ? 'success' : 'error')
}
return (
<form action={handleSubmit}>
<input type="file" name="file" required />
<button type="submit" disabled={status === 'uploading'}>
{status === 'uploading' ? '上传中...' : '上传'}
</button>
</form>
)
}
六、客户端刷新 #
6.1 router.refresh() #
tsx
'use client'
import { useRouter } from 'next/navigation'
export default function RefreshButton() {
const router = useRouter()
return (
<button onClick={() => router.refresh()}>
刷新数据
</button>
)
}
6.2 刷新特定路由 #
tsx
'use client'
import { useRouter } from 'next/navigation'
export default function RefreshPostButton({ slug }: { slug: string }) {
const router = useRouter()
const handleRefresh = async () => {
await fetch(`/api/revalidate?path=/blog/${slug}`, { method: 'POST' })
router.refresh()
}
return (
<button onClick={handleRefresh}>
刷新文章
</button>
)
}
七、错误处理 #
7.1 Server Action错误 #
tsx
'use server'
export async function createPost(formData: FormData) {
try {
await db.post.create({
data: {
title: formData.get('title') as string,
content: formData.get('content') as string,
},
})
revalidatePath('/blog')
return { success: true }
} catch (error) {
return { error: '创建失败' }
}
}
7.2 客户端错误处理 #
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" />
<textarea name="content" />
<button type="submit" disabled={isPending}>
发布
</button>
{state?.error && (
<p className="text-red-500">{state.error}</p>
)}
{state?.success && (
<p className="text-green-500">发布成功!</p>
)}
</form>
)
}
八、最佳实践 #
8.1 组织Server Actions #
text
app/
├── actions/
│ ├── posts.ts
│ ├── users.ts
│ └── comments.ts
└── blog/
└── page.tsx
8.2 类型安全 #
tsx
interface PostData {
title: string
content: string
}
export async function createPost(data: PostData) {
await db.post.create({ data })
revalidateTag('posts')
}
8.3 权限验证 #
tsx
'use server'
import { auth } from '@/auth'
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,
content: formData.get('content') as string,
authorId: session.user.id,
},
})
revalidateTag('posts')
}
九、总结 #
数据变更与重新验证要点:
| 要点 | 说明 |
|---|---|
| Server Actions | 服务端函数 |
| revalidatePath | 路径重新验证 |
| revalidateTag | 标签重新验证 |
| 乐观更新 | useOptimistic |
| 表单处理 | 表单验证和提交 |
| 错误处理 | try/catch和返回错误 |
下一步,让我们学习渲染策略!
最后更新:2026-03-28