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

如何在Zod + React Hook Form中仅在表单提交后对嵌套对象字段执行条件验证?

如何在Zod + React Hook Form中仅在表单提交后对嵌套对象字段执行条件验证?

我完全理解你的困扰——切换重复类型时立刻触发验证,用户还没来得及选择必填项就看到错误提示,体验确实很差。结合你的Zod和React Hook Form(RHF)代码,我给你几个针对性的解决方案,帮你实现「仅提交后验证」的预期效果:


一、核心优化思路

我们要实现三个关键目标:

  1. 仅在用户点击提交按钮后才执行全量验证
  2. 根据当前选中的repetition_type,仅验证对应必填字段,忽略无关字段
  3. 避免切换类型时主动触发验证,只依赖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>
  )}
/>

额外优化建议

  1. 用RHF的watch替代本地state:上面的代码已经用form.watch同步了重复类型的状态,避免了本地state和表单状态不一致的问题,更可靠。
  2. 添加重置逻辑:如果用户需要重置表单,可以调用form.reset(),会自动清空错误状态和表单值。
  3. 可选:触碰后验证:如果想在用户触碰过某个字段后也显示错误,可以把判断条件改成form.formState.isSubmitted || form.formState.touchedFields.repetition。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.07 09:39:35