如何使用Zod和react-hook-form实现多步骤表单分步验证
多步骤表单分步验证方案(基于Zod + React Hook Form)
核心思路
将整体表单Schema拆分为对应每个步骤的子Schema,在切换到下一步前仅验证当前步骤的必填字段,验证通过后才允许进入下一级。这样用户无法跳过未完成的步骤,错误信息也会即时显示在当前步骤中。
具体实现步骤
1. 拆分Zod Schema为步骤子Schema
把原有的formSchema拆分成与5个步骤对应的子Schema,明确每个步骤需要验证的字段:
// 步骤0:基本职位信息 const step0Schema = z.object({ jobTitle: z.string().min(2, { message: "Job Title must be at least 2 characters long" }), companyName: z.string().min(2, { message: "Company Name must be at least 2 characters long" }), jobLocation: z.string().min(2, { message: "Job Location must be at least 2 characters long" }), jobDescription: z.string().min(5, { message: "Job Description must be at least 5 characters long" }), jobType: z.string(), }); // 步骤1:申请详情 const step1Schema = z.object({ applicationDate: z.string(), applicationStatus: z.string(), applicationLink: z.string().optional(), }); // 步骤2:附加信息 const step2Schema = z.object({ applicationNotes: z.string().optional(), jobReferenceNumber: z.string().optional(), applicationDeadline: z.string().optional().refine((value) => !value || /^\d{4}-\d{2}-\d{2}$/.test(value), { message: "Application Deadline must be a valid date (YYYY-MM-DD)", }), }); // 步骤3:文件上传 const step3Schema = z.object({ resume: z.string().optional(), coverLetter: z.string().optional(), }); // 步骤4:推荐信息 const step4Schema = z.object({ referral: z.boolean().default(false), referralSource: z.string().optional(), referralContact: z.string().optional(), }); // 保持整体Schema用于最终提交验证 const formSchema = z.object({ ...step0Schema.shape, ...step1Schema.shape, ...step2Schema.shape, ...step3Schema.shape, ...step4Schema.shape, });
2. 定义各步骤对应字段列表
创建每个步骤对应的字段数组,方便触发针对性验证:
const stepFields = [ ['jobTitle', 'companyName', 'jobLocation', 'jobDescription', 'jobType'], // 步骤0 ['applicationDate', 'applicationStatus', 'applicationLink'], // 步骤1 ['applicationNotes', 'jobReferenceNumber', 'applicationDeadline'], // 步骤2 ['resume', 'coverLetter'], // 步骤3 ['referral', 'referralSource', 'referralContact'], // 步骤4 ];
3. 修改Next按钮的验证逻辑
在AddApplicationForm组件中,替换Next按钮的点击事件,先验证当前步骤字段,通过后再切换步骤:
// 步骤切换验证逻辑 const handleNext = async () => { // 触发当前步骤所有字段的验证 const isValid = await form.trigger(stepFields[currentStep]); if (isValid) { setCurrentStep(prev => prev + 1); } }; // 在return中的Next按钮部分替换为: <Button key="next" type="button" onClick={handleNext} > Next </Button>
4. 可选优化:推荐信息的条件验证
如果需要在用户勾选referral为true时强制填写referralSource和referralContact,可以修改step4Schema:
const step4Schema = z.object({ referral: z.boolean().default(false), referralSource: z.string().optional(), referralContact: z.string().optional(), }).refine((data) => { if (data.referral) { return data.referralSource?.length && data.referralContact?.length; } return true; }, { message: "Referral Source and Contact are required when referral is enabled", path: ["referralSource"], // 指定错误显示的字段 });
修改后的组件核心代码
整合后的AddApplicationForm关键部分:
export const AddApplicationForm = ({ onOpenChange }: { onOpenChange: any }) => { const queryClient = useQueryClient(); const [currentStep, setCurrentStep] = useState(0); const { user } = useUser(); // 保留原有的mutation逻辑 const { data, mutateAsync, isPending } = useMutation({ mutationFn: (formData: FormData) => createApplication(formData), onError: (error) => alert(error.message || "Failed to update"), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["applications"] }); onOpenChange(false); }, }); // 步骤字段列表 const stepFields = [ ['jobTitle', 'companyName', 'jobLocation', 'jobDescription', 'jobType'], ['applicationDate', 'applicationStatus', 'applicationLink'], ['applicationNotes', 'jobReferenceNumber', 'applicationDeadline'], ['resume', 'coverLetter'], ['referral', 'referralSource', 'referralContact'], ]; // 步骤切换验证逻辑 const handleNext = async () => { const isValid = await form.trigger(stepFields[currentStep]); if (isValid) { setCurrentStep(prev => prev + 1); } }; // 保留原有的form定义和onSubmit逻辑 const form = useForm<z.infer<typeof formSchema>>({ resolver: zodResolver(formSchema), defaultValues: { jobTitle: "", companyName: "", jobLocation: "", jobDescription: "", jobType: "part-time", applicationDate: "", applicationStatus: "applied", applicationLink: "", applicationNotes: "", jobReferenceNumber: "", applicationDeadline: "", resume: "", coverLetter: "", referral: false, referralSource: "", referralContact: "", }, }); async function onSubmit(values: FormValues) { const formData: FormData = new FormData(); const mongoUser = await findUserByClerkId(user?.id); formData.append("userId", mongoUser?._id); (Object.keys(values) as Array<keyof FormValues>).forEach((key) => { const value = values[key]; if (value !== undefined && value !== null) { formData.append(key, value.toString()); } }); mutateAsync(formData); } return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> {currentStep === 0 && BasicJobInfo(form)} {currentStep === 1 && ApplicationDetails(form)} {currentStep === 2 && AdditionalInformation(form)} {currentStep === 3 && uploads(form)} {currentStep === 4 && referralInfo(form)} <div className="flex justify-between"> {currentStep > 0 ? ( <Button type="button" onClick={() => setCurrentStep((prev) => prev - 1)} > Previous </Button> ) : ( <div /> )} {currentStep === 4 ? ( <Button key="submit" type="submit"> Submit </Button> ) : ( <Button key="next" type="button" onClick={handleNext} > Next </Button> )} </div> </form> </Form> ); };
效果说明
- 点击Next时,当前步骤所有字段会被验证,错误信息即时显示在对应字段下方
- 只有当前步骤验证通过后,才能进入下一步,避免跳过未完成步骤
- 最终提交时,自动验证整个表单Schema,确保所有必填项正确填写
内容的提问来源于stack exchange,提问作者RakibulB
相关产品推荐
相关产品推荐

