基于React Hook Form和Zod的多步骤表单实时验证问题
解决React Hook Form + Zod多步骤表单的当前步骤验证问题
核心思路
要实现当前步骤字段的独立验证,关键是拆分验证逻辑到单个步骤,不依赖全局的isValid(它对应整个表单的状态),而是通过「当前步骤字段的错误状态+交互状态」或者「手动触发步骤级验证」来控制按钮禁用状态。
具体实现方案
1. 拆分Zod Schema为步骤级
先把每个步骤的字段单独定义Schema,再合并为完整表单Schema,既支持步骤独立验证,也能保证最终提交的数据完整性:
import { z } from "zod"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; // 步骤1专属Schema const step1Schema = z.object({ firstName: z.string().min(2, "名字至少2个字符"), lastName: z.string().min(2, "姓氏至少2个字符"), }); // 步骤2专属Schema const step2Schema = z.object({ email: z.string().email("请输入有效的邮箱"), phone: z.string().regex(/^1[3-9]\d{9}$/, "请输入有效的手机号"), }); // 完整表单Schema(用于最终提交验证) const fullFormSchema = step1Schema.merge(step2Schema); type FormValues = z.infer<typeof fullFormSchema>;
2. 配置表单与步骤状态
使用useState管理当前步骤,表单配置mode满足即时验证需求:
import { useState } from "react"; export default function MultiStepForm() { const [currentStep, setCurrentStep] = useState(1); const { register, formState: { errors, touchedFields }, trigger, // 手动触发指定字段验证 getValues, // 获取指定字段的值 } = useForm<FormValues>({ resolver: zodResolver(fullFormSchema), mode: "onTouched", // 失焦时触发验证,若要输入时即时验证改为"onChange" });
3. 定义当前步骤字段与验证逻辑
编写工具函数获取当前步骤的字段列表,然后判断这些字段是否全部验证通过:
// 获取当前步骤的所有字段名 const getCurrentStepFields = () => { switch (currentStep) { case 1: return ["firstName", "lastName"]; case 2: return ["email", "phone"]; default: return []; } }; // 判断当前步骤是否满足激活下一步的条件 const isCurrentStepValid = () => { const currentFields = getCurrentStepFields(); // 逻辑:用户已交互过该字段,且字段无错误 return currentFields.every(field => touchedFields[field] && !errors[field]); };
4. 渲染步骤与控制按钮
根据当前步骤渲染对应字段,按钮禁用状态由isCurrentStepValid控制,点击下一步时手动触发当前步骤的全量验证:
const handleNext = async () => { // 手动触发当前步骤所有字段的验证(即使未交互过的字段也会验证) const stepIsValid = await trigger(getCurrentStepFields()); if (stepIsValid) { setCurrentStep(prev => prev + 1); } }; return ( <div className="multi-step-form"> {/* 步骤1渲染 */} {currentStep === 1 && ( <div className="step-content"> <div className="form-group"> <label>名字</label> <input {...register("firstName")} /> {errors.firstName && <p className="error-text">{errors.firstName.message}</p>} </div> <div className="form-group"> <label>姓氏</label> <input {...register("lastName")} /> {errors.lastName && <p className="error-text">{errors.lastName.message}</p>} </div> </div> )} {/* 步骤2渲染 */} {currentStep === 2 && ( <div className="step-content"> <div className="form-group"> <label>邮箱</label> <input {...register("email")} /> {errors.email && <p className="error-text">{errors.email.message}</p>} </div> <div className="form-group"> <label>手机号</label> <input {...register("phone")} /> {errors.phone && <p className="error-text">{errors.phone.message}</p>} </div> </div> )} <button onClick={handleNext} disabled={!isCurrentStepValid()} className="next-btn" > 下一步 </button> </div> ); }
可选优化:无交互时也验证字段值
如果希望用户未交互字段,但输入值符合规则时也激活按钮,可以修改isCurrentStepValid的逻辑,直接用步骤Schema验证字段值:
const isCurrentStepValid = () => { const currentFields = getCurrentStepFields(); const currentValues = Object.fromEntries( currentFields.map(field => [field, getValues(field)]) ); const currentSchema = currentStep === 1 ? step1Schema : step2Schema; return currentSchema.safeParse(currentValues).success; };
内容的提问来源于stack exchange,提问作者Linh Tran
相关产品推荐
相关产品推荐

