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") }), }); };
问题原因
- 验证模式配置冲突:设置了
mode: "all",该模式会在所有表单交互(包括按钮点击、输入框失焦等)时触发全量字段验证,直接覆盖了trigger手动验证指定字段的逻辑,导致点击Next时所有字段都被校验。 - Schema存在未渲染的必填字段:Schema中定义了
country为必填字段,但在步骤中没有对应的输入组件,该字段始终缺失,Zod全局校验时会判定整个表单无效,进而触发所有字段的错误状态。 - 全局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
相关产品推荐
相关产品推荐

