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

如何使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 03:28:12