如何用Next.js 14 Server Actions优雅处理多表单数据库更新
优化Next.js 14 Server Actions + useFormState的单字段表单更新代码
我在UI控制台里有「编辑标题」「编辑描述」等多个表单,每个表单只包含一个输入元素。现在用Next.js 14的Server Actions处理数据库更新,前端用useFormState做状态管理,功能已经实现,但代码重复冗余,想要更优雅的写法。
原始代码
const VideoUpdateSchemas = { title: z.object({ title: z.string().min(1, { message: "Feld darf nicht leer sein." }), }), description: z.object({ description: z.string().min(1, { message: "Feld darf nicht leer sein." }), }), }; export type UpdateState = { message?: string; errorsTitle?: { title?: string[]; }; errorsDescription?: { description?: string[]; }; success: boolean; status?: number; }; export async function updateVideo(prevState: UpdateState, formData: FormData) { const rawFormData = Object.fromEntries(formData.entries()); const videoId = rawFormData.id as string; delete rawFormData.id; let validatedFields; if (typeof rawFormData.title === "string") { validatedFields = VideoUpdateSchemas.title.safeParse({ title: formData.get("title"), }); if (!validatedFields?.success) { return { errorsTitle: validatedFields?.error.flatten().fieldErrors, success: false, }; } } if (typeof rawFormData.description === "string") { validatedFields = VideoUpdateSchemas.description.safeParse({ description: formData.get("description"), }); if (!validatedFields?.success) { return { errorsDescription: validatedFields?.error.flatten().fieldErrors, success: false, }; } } try { const video = await prisma.video.update({ where: { userId, id: videoId, }, data: { ...rawFormData, }, }); } catch (error) { return { success: false, }; } revalidatePath(`/dashboard/videos/${videoId}`); return { success: true, } }
优化思路与实现
1. 重构Zod Schema,消除重复
抽离通用验证规则,动态生成字段Schema,避免每个字段重复编写z.object()结构。
2. 统一错误状态结构
将分散的errorsTitle、errorsDescription合并为一个errors对象,键对应字段名,减少类型定义冗余。
3. 批量处理字段验证
通过一次解析所有表单数据,替代多分支if判断,简化验证流程。
4. 自动推导类型
利用Zod的z.infer自动生成数据类型,减少手动维护类型的工作量。
优化后的完整代码
import { z } from "zod"; import { revalidatePath } from "next/cache"; import prisma from "@/lib/prisma"; // 替换为你的Prisma实例路径 // 抽离通用必填字符串验证规则 const requiredString = z.string().min(1, { message: "字段不能为空。" }); // 统一定义更新Schema,支持多字段可选更新 const VideoUpdateSchema = z.object({ title: requiredString.optional(), description: requiredString.optional(), id: z.string(), // 视频ID为必填项 }); // 自动推导表单数据类型 type VideoUpdateData = z.infer<typeof VideoUpdateSchema>; // 统一状态类型结构 export type UpdateState = { message?: string; errors?: Record<string, string[]>; // 键为字段名,值为错误信息数组 success: boolean; status?: number; }; export async function updateVideo(prevState: UpdateState, formData: FormData) { try { // 解析并验证所有表单数据 const parsedResult = VideoUpdateSchema.safeParse(Object.fromEntries(formData.entries())); if (!parsedResult.success) { // 返回扁平化的错误结构 return { errors: parsedResult.error.flatten().fieldErrors, success: false, }; } const { id: videoId, ...updateData } = parsedResult.data; // 防止空提交:确保至少有一个字段需要更新 if (Object.keys(updateData).length === 0) { return { message: "请输入要更新的内容", success: false, }; } // 执行数据库更新 await prisma.video.update({ where: { userId, // 假设userId已通过鉴权逻辑获取 id: videoId, }, data: updateData, }); // 重新验证页面路径,更新缓存 revalidatePath(`/dashboard/videos/${videoId}`); return { success: true }; } catch (error) { return { message: "更新失败,请稍后重试", success: false, }; } }
优化点说明
- Schema复用:新增字段时只需在
VideoUpdateSchema中添加,无需重复编写验证逻辑。 - 类型自动维护:通过Zod推导数据类型,避免手动更新
UpdateState中的错误字段定义。 - 简化前端逻辑:前端可通过
errors?.title、errors?.description直接获取对应字段错误,状态判断更简洁。 - 增加边界处理:新增空提交判断,避免无效数据库请求,提升用户体验。
内容的提问来源于stack exchange,提问作者WorkoutBuddy
相关产品推荐
相关产品推荐

