Next.js 14(App Router)中Zod+Server Actions表单错误处理咨询
整合Zod表单验证与错误处理方案
步骤1:安装Zod
首先安装Zod依赖:
npm install zod # 或 yarn add zod
步骤2:创建Zod验证Schema
在@/app/schemas/user.ts中定义用户注册的验证规则:
import { z } from "zod"; export const registerSchema = z.object({ firstName: z.string().min(2, "First name must be at least 2 characters"), lastName: z.string().min(2, "Last name must be at least 2 characters"), email: z.string().email("Invalid email address"), password: z.string().min(6, "Password must be at least 6 characters") }); export type RegisterSchema = z.infer<typeof registerSchema>;
步骤3:修改Server Action处理验证与错误
更新@/app/actions.ts,加入Zod验证,并区分字段验证错误和服务器端错误:
"use server"; import { createUser } from "@/db/queries"; import { redirect } from "next/navigation"; import { registerSchema } from "./schemas/user"; import { ZodError } from "zod"; export async function registerUser(prevState: any, formData: FormData) { try { // 解析并验证表单数据 const validatedData = registerSchema.parse(Object.fromEntries(formData)); // 插入MongoDB const created = await createUser(validatedData); if (created) { redirect("/login"); } } catch (error) { // 处理Zod字段验证错误 if (error instanceof ZodError) { const fieldErrors = error.issues.reduce((acc, issue) => { acc[issue.path[0] as string] = issue.message; return acc; }, {} as Record<string, string>); return { type: "field-error", errors: fieldErrors, message: "" }; } // 处理MongoDB重复邮箱错误(E11000重复键) if (error.code === 11000) { return { type: "server-error", errors: {}, message: "Email already exists" }; } // 其他服务器端错误 return { type: "server-error", errors: {}, message: "Something went wrong, please try again" }; } }
步骤4:更新表单组件展示错误
将表单组件改为客户端组件,使用useActionState获取错误状态,分别展示顶部的服务器错误和字段下方的验证错误:
"use client"; import { useActionState } from "react"; import { registerUser } from "@/app/actions"; const initialState = { type: "", errors: {}, message: "" }; export default function RegistrationForm() { const [state, formAction] = useActionState(registerUser, initialState); return ( <form className='login-form' action={formAction}> {/* 服务器端错误展示在表单顶部 */} {state.type === "server-error" && state.message && ( <div className="text-red-500 mb-4"> {state.message} </div> )} <div> <label htmlFor='fname'>First Name</label> <input type='text' name='firstName' id='fname' className={state.errors.firstName ? "border-red-500" : ""} /> {/* 字段验证错误展示在输入框下方 */} {state.errors.firstName && ( <p className="text-red-500 text-sm mt-1"> {state.errors.firstName} </p> )} </div> <div> <label htmlFor='lname'>Last Name</label> <input type='text' name='lastName' id='lname' className={state.errors.lastName ? "border-red-500" : ""} /> {state.errors.lastName && ( <p className="text-red-500 text-sm mt-1"> {state.errors.lastName} </p> )} </div> <div> <label htmlFor='email'>Email Address</label> <input type='email' name='email' id='email' className={state.errors.email ? "border-red-500" : ""} /> {state.errors.email && ( <p className="text-red-500 text-sm mt-1"> {state.errors.email} </p> )} </div> <div> <label htmlFor='password'>Password</label> <input type='password' name='password' id='password' className={state.errors.password ? "border-red-500" : ""} /> {state.errors.password && ( <p className="text-red-500 text-sm mt-1"> {state.errors.password} </p> )} </div> <button type='submit' className='bg-[#eb4a36] py-3 rounded-md text-white w-full mt-4' > Create Account </button> </form> ); }
步骤5:确保MongoDB错误正确抛出
createUser函数保持原样即可,错误会被Server Action捕获处理:
async function createUser(user) { try { await connectMongo(); return await userModel.create(user); } catch (error) { throw error; } } export { createUser };
关键说明
- 使用
useActionState保存表单提交后的错误状态,这是Next.js 14 App Router处理表单错误的推荐方式 - Zod验证在Server Action中执行,避免客户端验证被绕过
- 区分字段级错误和服务器错误,分别展示在对应位置
- 处理了MongoDB重复邮箱场景,给出友好提示
内容的提问来源于stack exchange,提问作者Joyram Das
相关产品推荐
相关产品推荐

