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

React Hook Form与Zod自定义错误的设置与清除方法

实现表单字段的条件验证与错误控制

需求说明

需要实现仅在选择对应支付方式时,才验证该方式下的必填字段;切换支付方式时自动清除无关字段的错误,避免提交时触发不必要的验证失败。默认Zod验证无法直接支持这种条件逻辑,因此需要自定义验证规则。

解决方案步骤

1. 修改Zod Schema,添加条件验证逻辑

将原本的必填字段改为可选,通过superRefine添加自定义条件验证,根据当前选择的支付方式,对对应字段进行校验并抛出针对性错误。

2. 切换支付方式时清除无关字段错误

在支付方式选择框的onChange事件中,调用react-hook-form的clearErrors方法清除无关字段的错误状态,同时可清空字段值避免残留数据干扰。

3. 保留原有表单字段渲染逻辑

确保仅在对应支付方式下渲染字段,并正常显示该字段的错误提示。

修改后的代码示例

自定义Zod Schema

import { z } from "zod";

const FormSchema = z.object({
  modeOfPayment: z.string().nonempty({ message: "请选择支付方式" }),
  tillNumber: z.string().optional(),
  paybillNumber: z.string().optional(),
  accountNumber: z.string().optional(),
}).superRefine((data, ctx) => {
  // 验证Mpesa Buy Goods对应的字段
  if (data.modeOfPayment === "Mpesa Buy Goods") {
    if (!data.tillNumber) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "请输入编号",
        path: ["tillNumber"],
      });
    } else if (data.tillNumber.length < 1) {
      ctx.addIssue({
        code: z.ZodIssueCode.too_small,
        minimum: 1,
        type: "string",
        message: "请提供有效值",
        path: ["tillNumber"],
      });
    } else if (data.tillNumber.length > 8) {
      ctx.addIssue({
        code: z.ZodIssueCode.too_big,
        maximum: 8,
        type: "string",
        message: "请输入有效的编号",
        path: ["tillNumber"],
      });
    }
  } 
  // 验证Mpesa Paybill对应的字段
  else if (data.modeOfPayment === "Mpesa Paybill") {
    // 校验paybillNumber
    if (!data.paybillNumber) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "请输入账单编号",
        path: ["paybillNumber"],
      });
    } else if (data.paybillNumber.length < 1) {
      ctx.addIssue({
        code: z.ZodIssueCode.too_small,
        minimum: 1,
        type: "string",
        message: "请提供有效值",
        path: ["paybillNumber"],
      });
    } else if (data.paybillNumber.length > 8) {
      ctx.addIssue({
        code: z.ZodIssueCode.too_big,
        maximum: 8,
        type: "string",
        message: "请输入有效的账单编号",
        path: ["paybillNumber"],
      });
    }

    // 校验accountNumber
    if (!data.accountNumber) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "请输入账户编号",
        path: ["accountNumber"],
      });
    } else if (data.accountNumber.length < 1) {
      ctx.addIssue({
        code: z.ZodIssueCode.too_small,
        minimum: 1,
        type: "string",
        message: "请提供有效值",
        path: ["accountNumber"],
      });
    } else if (data.accountNumber.length > 20) {
      ctx.addIssue({
        code: z.ZodIssueCode.too_big,
        maximum: 20,
        type: "string",
        message: "请输入有效的账户编号",
        path: ["accountNumber"],
      });
    }
  }
});

支付方式选择组件(修改onChange逻辑)

<div>
  <h2>
    支付方式
    <span className="text-red-700 ml-1">*</span>
  </h2>
  <select
    {...register("modeOfPayment", {
      onChange: (e) => {
        const selectedValue = e.target.value;
        setSelectedPaymentMode(selectedValue);
        
        // 清除无关字段的错误和值
        if (selectedValue !== "Mpesa Buy Goods") {
          clearErrors("tillNumber");
          setValue("tillNumber", "");
        }
        if (selectedValue !== "Mpesa Paybill") {
          clearErrors("paybillNumber");
          clearErrors("accountNumber");
          setValue("paybillNumber", "");
          setValue("accountNumber", "");
        }
      },
    })}
    className={/* 保留原有className */}
  >
    <option value="" disabled={true}>
      选择支付方式
    </option>
    <option value="Mpesa Buy Goods">Mpesa 购买商品</option>
    <option value="Mpesa Paybill">Mpesa 账单支付</option>
  </select>
  {errors?.modeOfPayment?.message && (
    <p className=" text-red-700 mt-1">
      {errors.modeOfPayment.message}
    </p>
  )}
</div>

Mpesa Buy Goods字段(保留原有渲染)

{selectedPaymentMode === "Mpesa Buy Goods" && (
  <div>
    <h2>
      输入编号
      <span className="text-red-700 ml-1">*</span>
    </h2>
    <input
      {...restTillNumber}
      ref={(e) => {
        tillNoRef(e);
        tillNumberRef.current = e;
      }}
      type="text"
      className={/* 保留原有className */}
    />
    {errors?.tillNumber?.message && (
      <p className=" text-red-700 mt-1">
        {errors.tillNumber.message}
      </p>
    )}
  </div>
)}

Mpesa Paybill字段(打开错误提示注释)

{selectedPaymentMode === "Mpesa Paybill" && (
  <div>
    <div className="mb-2">
      <h2>
        输入账单编号
        <span className="text-red-700 ml-1">*</span>
      </h2>
      <input
        type="text"
        {...restPaybillNumber}
        ref={(e) => {
          paybillNoRef(e);
          paybillNumberRef.current = e;
        }}
        className={/* 保留原有className */}
      />
      {errors?.paybillNumber?.message && (
        <p className="text-red-700 mt-1">
          {errors.paybillNumber.message}
        </p>
      )}
    </div>
    <div>
      <h2>
        账户编号
        <span className="text-red-700 ml-1">*</span>
      </h2>
      <input
        type="text"
        {...restAccountNumber}
        ref={(e) => {
          accountNoRef(e);
          accountNumberRef.current = e;
        }}
        className={`h-8 w-11/12 self-center text-sm text-gray-800 ${
          errors?.accountNumber?.message
            ? "border-red-700 focus:border-none focus:ring focus:ring-red-700 focus:ring-2"
            : "focus:border-none focus:ring focus:ring-emerald-700 focus:ring-2"
        }`}
      />
      {errors?.accountNumber?.message && (
        <p className="text-red-700 mt-1">
          {errors.accountNumber.message}
        </p>
      )}
    </div>
  </div>
)}

说明

  • 改用superRefine后,可根据支付方式动态校验对应字段,错误信息会精准关联到对应字段路径。
  • 切换支付方式时清除无关字段的错误和值,确保表单状态干净,避免提交时触发不必要的验证。
  • 原有字段渲染逻辑无需大幅修改,仅需打开Paybill字段的错误提示注释即可。

内容的提问来源于stack exchange,提问作者Dave Odipo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 14:02:02