如何在Zod + React Hook Form中仅在表单提交后对嵌套对象字段执行条件验证?
如何在Zod + React Hook Form中仅在表单提交后对嵌套对象字段执行条件验证?
我完全理解你的困扰——切换重复类型时立刻触发验证,用户还没来得及选择必填项就看到错误提示,体验确实很差。结合你的Zod和React Hook Form(RHF)代码,我给你几个针对性的解决方案,帮你实现「仅提交后验证」的预期效果:
一、核心优化思路
我们要实现三个关键目标:
- 仅在用户点击提交按钮后才执行全量验证
- 根据当前选中的
repetition_type,仅验证对应必填字段,忽略无关字段 - 避免切换类型时主动触发验证,只依赖RHF的提交触发机制
二、步骤1:优化Zod Schema,实现条件式必填验证
你的原Schema没有根据repetition_type做条件校验,我们用superRefine来实现「不同类型对应不同必填字段」的逻辑,同时确保非当前类型的字段不会被校验:
import { z } from "zod"; const BasicRepetitionSchema = z.object({ repetition_type: BasicRepetitionKindSchema, week_days: z.string().array(), month_days: z.string().array(), months: z.string().array(), }).superRefine((data, ctx) => { // 根据选中的重复类型,仅验证对应字段 switch (data.repetition_type) { case "custom_weekdays": if (data.week_days.length === 0) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "请选择至少一个工作日", path: ["week_days"], // 指定错误对应字段,方便RHF定位 }); } break; case "day_of_month": if (data.month_days.length === 0) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "请选择至少一个每月日期", path: ["month_days"], }); } if (data.months.length === 0) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "请选择至少一个月份", path: ["months"], }); } break; case "monthly": // 假设你的类型包含这个选项 if (data.months.length === 0) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "请选择至少一个月份", path: ["months"], }); } break; } });
三、步骤2:配置React Hook Form,关闭实时验证
在创建form实例时,设置mode: "onSubmit",让RHF默认仅在提交时触发验证,不会在字段变化时自动校验:
import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; const form = useForm({ defaultValues: { repetition: { repetition_type: null, // 或者你的默认类型 week_days: [], month_days: [], months: [], }, }, mode: "onSubmit", // 关键:仅在提交时验证 resolver: zodResolver(BasicRepetitionSchema), });
四、步骤3:修改Repetition组件,移除主动触发验证的逻辑
你的原组件中调用了trigger("repetition")和setValue时传shouldValidate: true,这会导致切换类型时立刻触发验证,我们需要删掉这些逻辑:
export const Repetition = ({ form }: { form: UseFormReturn<{ repetition: BasicRepetitionPropT }> }) => { // 用RHF的watch同步表单状态,无需自己维护useState const repetition = form.watch("repetition"); const repType = repetition.repetition_type; const updateRepetition = (updates: Partial<BasicRepetitionPropT>) => { form.setValue("repetition", { ...repetition, ...updates }); // 去掉shouldValidate: true,让RHF不立刻验证 }; const changeRepetitionHandler = (value: BasicRepetitionKindType) => { updateRepetition({ repetition_type: value, week_days: [], month_days: [], months: [], }); // 删掉trigger("repetition"),禁止主动触发验证 }; return ( <> <Select onValueChange={changeRepetitionHandler} defaultValue={repType || ""}> <SelectTrigger> <SelectValue placeholder="Select Repetition" /> </SelectTrigger> <SelectContent> {BasicRepetitonKindsValues.map((item) => ( <SelectItem key={item} value={item}> {repetitionLabels[item]} </SelectItem> ))} </SelectContent> </Select> <div> {repType === "custom_weekdays" && ( <SelectDropDown items={WEEK_DAYS} selectedItems={repetition.week_days} onChange={(newDays) => updateRepetition({ week_days: newDays })} title="Select week days" /> )} {(repType === "day_of_month" || repType === "monthly") && ( <SelectDropDown items={MONTHS} selectedItems={repetition.months} onChange={(newMonths) => updateRepetition({ months: newMonths })} title="Select months" /> )} {repType === "day_of_month" && ( <SelectDropDown items={MONTH_DAYS} selectedItems={repetition.month_days} onChange={(newMonthDays) => updateRepetition({ month_days: newMonthDays })} title="Select month days" /> )} </div> </> ); };
五、步骤4:优化错误提示逻辑,仅在提交后显示对应类型的错误
修改Shadcn的FormField,绑定formState.isSubmitted判断,仅在用户提交后才显示错误,同时只展示当前类型对应的字段错误:
<FormField name="repetition" control={form.control} render={({ field }) => ( <FormItem> <FormControl> <Repetition form={form} /> </FormControl> {/* 仅在表单已提交后,才显示对应类型的错误 */} {form.formState.isSubmitted && form.formState.errors.repetition && ( <FormMessage className="space-y-1 text-sm text-red-500"> {form.formState.errors.repetition.week_days?.message && repType === "custom_weekdays" && ( <p>{form.formState.errors.repetition.week_days.message}</p> )} {form.formState.errors.repetition.months?.message && (repType === "day_of_month" || repType === "monthly") && ( <p>{form.formState.errors.repetition.months.message}</p> )} {form.formState.errors.repetition.month_days?.message && repType === "day_of_month" && ( <p>{form.formState.errors.repetition.month_days.message}</p> )} </FormMessage> )} </FormItem> )} />
额外优化建议
- 用RHF的watch替代本地state:上面的代码已经用
form.watch同步了重复类型的状态,避免了本地state和表单状态不一致的问题,更可靠。 - 添加重置逻辑:如果用户需要重置表单,可以调用
form.reset(),会自动清空错误状态和表单值。 - 可选:触碰后验证:如果想在用户触碰过某个字段后也显示错误,可以把判断条件改成
form.formState.isSubmitted || form.formState.touchedFields.repetition。
内容来源于stack exchange
相关产品推荐
相关产品推荐

