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