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

React Hook Form Trigger异常:多步骤表单全字段验证问题

React Hook Form + Zod多步骤表单全量验证问题排查与修复

问题场景

使用React Hook Form结合Zod开发多步骤表单,预期点击「Next」按钮时仅验证当前步骤的字段,但实际会触发所有字段的验证,切换到下一步后,所有未填写的字段都显示错误状态。

相关代码片段

主页面核心代码

const steps = [
    { name: "Expertise Areas", fields: ["expertise_areas"] },
    { name: "Contact Info", fields: ["full_name", "email", "phone", "company"] },
    { name: "Project Details", fields: ["message"] }
]

const {
    register, handleSubmit, trigger, formState: { errors }} = useForm<fastTrackFormData>({
    resolver: zodResolver(FastTrackSchema),
    mode: "all"
});

const nextStep = async () => {
    const fields = steps[currentStep - 1].fields;
    const isValid = await trigger(fields as FieldName[], { shouldFocus: true });

    if (!isValid) return;

    if (currentStep < 3) {
      setCurrentStep(currentStep + 1);
    } else {
      await handleSubmit(onSubmit)();
    }
}

Zod Schema核心代码

const createFastTrackSchema = (t: (key: string) => string | undefined) => {
    return z.object({
        expertise_areas: expertise_areas,
        full_name: z.string().min(1, { message: t("fast_track.full_name_required") }),
        email: z.string()
            .min(1, { message: t("fast_track.email_required") })
            .email({ message: t("fast_track.email_invalid") }),
        phone: z.string().min(1, { message: t("fast_track.phone_required") }),
        company: z.string().min(1, { message: t("fast_track.company_required") }),
        country: z.string().min(1, { message: t("fast_track.country_required") }),
        message: z.string().min(1, { message: t("fast_track.message_required") }),
    });
};

问题原因

  1. 验证模式配置冲突:设置了mode: "all",该模式会在所有表单交互(包括按钮点击、输入框失焦等)时触发全量字段验证,直接覆盖了trigger手动验证指定字段的逻辑,导致点击Next时所有字段都被校验。
  2. Schema存在未渲染的必填字段:Schema中定义了country为必填字段,但在步骤中没有对应的输入组件,该字段始终缺失,Zod全局校验时会判定整个表单无效,进而触发所有字段的错误状态。
  3. 全局Schema未适配分步校验:当前使用的是完整的全局Schema,即使通过trigger指定了当前步骤字段,Zod配合zodResolver时仍会校验整个Schema结构,未填写的后续步骤字段会被判定为缺失,抛出错误。

修复方案

1. 调整React Hook Form验证模式

将mode改为"onSubmit",仅在最终提交时触发全量验证,手动调用trigger时仅验证指定字段:

const {
    register, handleSubmit, trigger, formState: { errors }} = useForm<fastTrackFormData>({
    resolver: zodResolver(FastTrackSchema),
    mode: "onSubmit" // 修改验证模式
});

2. 处理未渲染的必填字段

要么在步骤中添加country字段的输入组件,要么根据业务需求将其改为可选字段:

// Zod Schema中修改country字段
country: z.string().optional().or(z.literal("")) // 允许为空或可选

3. 实现分步校验的Schema逻辑

为每个步骤创建独立的Schema,确保分步验证时仅校验当前步骤的字段:

// 拆分步骤Schema
const step1Schema = z.object({
  expertise_areas: expertise_areas
});

const step2Schema = z.object({
  full_name: z.string().min(1, { message: t("fast_track.full_name_required") }),
  email: z.string().min(1, { message: t("fast_track.email_required") }).email({ message: t("fast_track.email_invalid") }),
  phone: z.string().min(1, { message: t("fast_track.phone_required") }),
  company: z.string().min(1, { message: t("fast_track.company_required") })
});

const step3Schema = z.object({
  message: z.string().min(1, { message: t("fast_track.message_required") }),
  country: z.string().min(1, { message: t("fast_track.country_required") })
});

// 修改nextStep逻辑
const nextStep = async () => {
  const { getValues, setErrors } = useForm<fastTrackFormData>({...}); // 解构getValues和setErrors
  const formValues = getValues();
  const currentStepSchema = [step1Schema, step2Schema, step3Schema][currentStep - 1];
  
  try {
    await currentStepSchema.parseAsync(formValues);
    // 触发当前字段验证,更新错误状态
    await trigger(steps[currentStep - 1].fields as FieldName[], { shouldFocus: true });
  } catch (err) {
    // 将Zod错误同步到React Hook Form的errors中
    if (err instanceof z.ZodError) {
      const formattedErrors = err.format();
      setErrors(formattedErrors as any);
    }
    return;
  }

  if (currentStep < 3) {
    setCurrentStep(currentStep + 1);
  } else {
    await handleSubmit(onSubmit)();
  }
};

或者使用Zod的partial()和pick()方法动态生成当前步骤的校验规则:

const nextStep = async () => {
  const { getValues } = useForm<fastTrackFormData>({...});
  const formValues = getValues();
  const currentFields = steps[currentStep - 1].fields;
  
  // 生成仅包含当前步骤字段的Schema
  const stepSchema = FastTrackSchema.partial().pick(Object.fromEntries(currentFields.map(f => [f, true])));
  
  try {
    await stepSchema.parseAsync(formValues);
    await trigger(currentFields as FieldName[], { shouldFocus: true });
  } catch (err) {
    return;
  }

  if (currentStep < 3) {
    setCurrentStep(currentStep + 1);
  } else {
    await handleSubmit(onSubmit)();
  }
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 11:05:54