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

多步骤表单仅提交最后一步的值——如何保留并提交所有步骤的值?

多步骤表单仅提交最后一步的值——如何保留并提交所有步骤的值?

嘿,我之前也踩过这个坑!用react-hook-form + Zod做可复用多步表单的时候,经常会碰到切换步骤后之前填的值就丢了,最后提交只拿到最后一步数据的问题。其实核心解决思路就是不要让每个步骤单独管理表单状态,而是用一个全局的表单实例来统一存所有步骤的字段值,再配合Zod的schema组合来做验证。下面给你几个具体的实现方案,都是实际项目里能用的:

1. 统一用一个全局Form实例(最推荐的基础方案)

不要在每个步骤组件里单独调用useForm,而是在最外层的表单容器里初始化一次,把register、control这些方法传递给各个步骤组件。这样所有步骤的字段都会存在同一个form state里,切换步骤时自然不会丢失之前的值。

举个具体的代码例子:
首先组合所有步骤的Zod schema成一个总schema,同时定义好所有字段的默认值:

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { useState } from 'react';

// 第一步的字段schema
const Step1Schema = z.object({
  firstName: z.string().min(1, '请填写名字'),
  lastName: z.string().min(1, '请填写姓氏'),
});

// 第二步的字段schema
const Step2Schema = z.object({
  email: z.string().email('邮箱格式不正确'),
  phone: z.string().min(10, '手机号长度不够'),
});

// 合并成总schema
const TotalFormSchema = Step1Schema.merge(Step2Schema);
type FormValues = z.infer<typeof TotalFormSchema>;

export default function MultiStepForm() {
  // 全局初始化一次useForm,用总schema做校验
  const { register, handleSubmit, formState: { errors } } = useForm<FormValues>({
    resolver: zodResolver(TotalFormSchema),
    defaultValues: {
      firstName: '',
      lastName: '',
      email: '',
      phone: '',
    },
  });

  const [currentStep, setCurrentStep] = useState(1);

  // 提交时就能拿到所有步骤的完整数据
  const onSubmit = (data: FormValues) => {
    console.log('所有步骤的提交数据:', data);
    // 这里写你的提交逻辑
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {currentStep === 1 && (
        <div className="step-container">
          <h3>第一步:基本信息</h3>
          <div>
            <label>名字:</label>
            <input {...register('firstName')} />
            {errors.firstName && <p className="error">{errors.firstName.message}</p>}
          </div>
          <div>
            <label>姓氏:</label>
            <input {...register('lastName')} />
            {errors.lastName && <p className="error">{errors.lastName.message}</p>}
          </div>
          <button type="button" onClick={() => setCurrentStep(2)}>下一步</button>
        </div>
      )}

      {currentStep === 2 && (
        <div className="step-container">
          <h3>第二步:联系方式</h3>
          <div>
            <label>邮箱:</label>
            <input {...register('email')} />
            {errors.email && <p className="error">{errors.email.message}</p>}
          </div>
          <div>
            <label>手机号:</label>
            <input {...register('phone')} />
            {errors.phone && <p className="error">{errors.phone.message}</p>}
          </div>
          <button type="button" onClick={() => setCurrentStep(1)}>上一步</button>
          <button type="submit">提交表单</button>
        </div>
      )}
    </form>
  );
}

这个方案的好处是逻辑简单,所有字段都在同一个状态里管理,提交时直接拿完整数据,而且Zod的校验也能覆盖所有字段。

2. 分步骤验证(想先过当前步才能进下一步的情况)

如果需要做“必须通过当前步骤的校验才能进入下一步”的逻辑,不用直接用总schema的resolver,而是在切换步骤时单独触发当前步骤字段的校验,提交时再校验所有字段。

比如修改上面的切换步骤逻辑:

// 全局初始化useForm时暂时不设置总resolver,或者用宽松的默认
const { register, handleSubmit, trigger, formState: { errors } } = useForm<FormValues>({
  defaultValues: {
    firstName: '',
    lastName: '',
    email: '',
    phone: '',
  },
});

// 切换到第二步前先校验第一步的字段
const goToStep2 = async () => {
  const isStep1Valid = await trigger(['firstName', 'lastName']);
  if (isStep1Valid) {
    setCurrentStep(2);
  }
};

// 切换回第一步直接切就行,不用校验
const goToStep1 = () => {
  setCurrentStep(1);
};

// 提交时校验所有字段
const onSubmit = handleSubmit(async (data) => {
  const isAllValid = await trigger();
  if (isAllValid) {
    console.log('所有校验通过的完整数据:', data);
    // 执行提交
  }
});

这样既实现了分步骤的校验拦截,又能保证所有步骤的字段值都被保留在全局form state里。

3. 适配你的自定义组件(FormWrapper、RHFInput)

确保你的自定义表单组件是接收外层传递的register、control等props,而不是在组件内部自己初始化useForm。比如你的RHFInput应该是这样的:

import { UseFormRegisterReturn } from 'react-hook-form';

type RHFInputProps = {
  name: string;
  register: UseFormRegisterReturn;
  label?: string;
};

export default function RHFInput({ name, register, label }: RHFInputProps) {
  return (
    <div className="form-input">
      {label && <label htmlFor={name}>{label}</label>}
      <input id={name} {...register} />
    </div>
  );
}

然后在步骤组件里使用时,传递外层的register方法:

// 在第一步里用RHFInput
<RHFInput name="firstName" register={register('firstName')} label="名字" />

这样自定义组件的字段也会注册到全局的form state里,不会出现独立的状态。

避坑提醒

  • 绝对不要在每个步骤组件里单独调用useForm,不然每个步骤都有自己的表单状态,切换时之前的数据肯定会丢。
  • 不要在切换步骤时调用reset方法,除非你真的想清空所有已填的值。
  • 如果是动态增加的步骤(比如用户可以添加多个地址),可以用react-hook-form的useFieldArray来管理动态字段,核心还是要基于全局的form state来操作。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 13:13:08