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
相关产品推荐
相关产品推荐

