You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 16:24:54