上节我们学习了数据获取,本节来聊聊数据变更(修改数据)。在实际应用中,我们不仅需要读取数据,还需要创建、更新、删除数据。Next.js 提供了几种方式来处理数据变更,每种都有自己的适用场景。
数据变更方式
在 Next.js 中,主要有三种方式来变更数据:
- Server Actions - Next.js 推荐的新方式,可以直接从组件调用服务器函数
- API 路由 - 传统的 REST API 端点
- 客户端 fetch - 从客户端发送请求到外部 API
大多数情况下,Server Actions 是最简洁的选择。
Server Actions
Server Actions 是 Next.js 13+ 引入的新特性,它允许你在服务器上执行的异步函数,可以直接从客户端或服务端组件调用。这比传统的 API 路由 + fetch 方式要简洁很多。
基本 Server Action
创建 Server Action 非常简单,只需要在文件顶部加上 'use server' 指令:
// app/actions.ts
'use server'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
// 验证数据
if (!title || !content) {
return { error: '标题和内容不能为空' }
}
// 保存到数据库
await db.post.create({
data: { title, content },
})
return { success: true }
}
在表单中使用
Server Action 最常见的用途就是处理表单提交:
// app/posts/page.tsx
import { createPost } from '@/app/actions'
export default function PostsPage() {
return (
<div>
<h1>创建文章</h1>
<form action={createPost}>
<input name="title" placeholder="标题" />
<textarea name="content" placeholder="内容"></textarea>
<button type="submit">提交</button>
</form>
</div>
)
}
带重定向的 Server Action
很多情况下,表单提交后需要跳转到其他页面:
// app/actions.ts
'use server'
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'
export async function updatePost(id: string, formData: FormData) {
const title = formData.get('title') as string
await db.post.update({
where: { id },
data: { title },
})
// 重新验证相关页面的缓存
revalidatePath('/posts')
revalidatePath(`/posts/${id}`)
// 重定向
redirect(`/posts/${id}`)
}
处理错误
数据验证和错误处理是必不可少的:
// app/actions.ts
'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 validatedFields = PostSchema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
})
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: '验证失败',
}
}
const { title, content } = validatedFields.data
try {
await db.post.create({ data: { title, content } })
return { message: '文章创建成功' }
} catch (error) {
return { message: '数据库错误: 无法创建文章' }
}
}
API 路由
虽然 Server Actions 是推荐方式,但有些场景下 API 路由仍然很有用,比如需要给第三方调用,或者需要标准的 REST API。
基本 API 路由
// app/api/posts/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
const posts = await db.post.findMany()
return NextResponse.json(posts)
}
export async function POST(request: Request) {
const body = await request.json()
const post = await db.post.create({
data: body,
})
return NextResponse.json(post, { status: 201 })
}
动态 API 路由
处理特定资源的 CRUD 操作:
// app/api/posts/[id]/route.ts
import { NextResponse } from 'next/server'
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const post = await db.post.findUnique({
where: { id: params.id },
})
if (!post) {
return NextResponse.json(
{ error: '文章不存在' },
{ status: 404 }
)
}
return NextResponse.json(post)
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
await db.post.delete({
where: { id: params.id },
})
return new NextResponse(null, { status: 204 })
}
乐观更新
乐观更新是一种让 UI 响应更快的技巧,它在等待服务器响应时先更新 UI,如果失败了再回滚:
'use client'
import { useOptimistic } from 'react'
import { likePost } from '@/app/actions'
export default function PostWithLike({ post }: { post: Post }) {
const [optimisticPost, addOptimisticPost] = useOptimistic(
post,
(state, newPost: Post) => {
return {
...state,
...newPost,
}
}
)
async function handleLike() {
addOptimisticPost({ ...post, likes: post.likes + 1 })
await likePost(post.id)
}
return (
<div>
<h2>{post.title}</h2>
<p>{optimisticPost.likes} 点赞</p>
<button onClick={handleLike}>点赞</button>
</div>
)
}
重新验证
当数据变更后,需要重新验证相关页面的缓存:
'use server'
import { revalidatePath } from 'next/cache'
export async function updatePost(id: string, data: FormData) {
// 更新文章
await db.post.update({
where: { id },
data: { title: data.get('title') },
})
// 重新验证相关页面
revalidatePath('/posts')
revalidatePath(`/posts/${id}`)
}
使用标签进行更精确的控制:
'use server'
import { revalidateTag } from 'next/cache'
export async function createComment() {
await db.comment.create(/* ... */)
// 重新验证多个缓存
revalidateTag('comments')
revalidateTag('posts') // 更新文章评论数
}
实用建议
这里分享几个在处理数据变更时特别实用的技巧。
使用 Server Actions 简化代码
实际开发中,我发现 Server Actions 通常是更好的选择,代码更简洁:
// 推荐这样做 - 使用 Server Actions
<form action={createPost}>
<input name="title" />
<button>提交</button>
</form>
// 传统方式也可以,但代码会复杂一些
<form onSubmit={handleSubmit}>
<input name="title" />
<button>提交</button>
</form>
始终验证输入
这个技巧特别重要——永远不要相信用户输入,一定要验证:
'use server'
import { z } from 'zod'
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
})
export async function register(formData: FormData) {
const validatedData = schema.parse({
email: formData.get('email'),
password: formData.get('password'),
})
// 然后可以安全地使用验证后的数据
}
处理错误状态
给用户明确的错误反馈,这个小细节能大大提升用户体验:
'use client'
import { useFormState } from 'react-dom'
export default function Form() {
const [state, formAction] = useFormState(action, initialState)
return (
<form action={formAction}>
{/* 表单字段 */}
{state.errors && <div>{state.errors}</div>}
</form>
)
}
总结
本节我们学习了 Next.js 中的数据变更方式,包括 Server Actions、API 路由、乐观更新等。Server Actions 是 Next.js 推荐的新方式,能让数据变更的代码更简洁。合理使用这些技术,能让你的应用交互更加流畅。
如果你对本节内容有任何疑问,欢迎在评论区提出来,我们一起学习讨论。