人人都会AI编程

复杂表单、动态表单、数组字段处理

更新时间:2026-07-11

在实际业务中,表单很少只是扁平的一层字段。常见的中等复杂度场景包括:嵌套对象(如收货地址包含省/市/详细地址)、动态列表(如添加多个团队成员、多个发票抬头)、以及可增删的数组字段。React Hook Form 通过其灵活的 register 机制和专门的 useFieldArray 钩子,让这类表单的处理变得十分简洁。

1. 复杂表单:嵌套对象与深层路径

对于嵌套的对象结构,React Hook Form 支持使用点号路径来注册字段,自动将表单数据组织成嵌套对象。例如,一个收货地址表单:

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

function AddressForm() {
  const { register, handleSubmit } = useForm({
    defaultValues: {
      address: {
        province: '',
        city: '',
        detail: ''
      }
    }
  });

  const onSubmit = (data) => console.log(data);
  // data.address = { province: '...', city: '...', detail: '...' }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('address.province')} placeholder="省" />
      <input {...register('address.city')} placeholder="市" />
      <input {...register('address.detail')} placeholder="详细地址" />
      <button type="submit">提交</button>
    </form>
  );
}

注意点:

  • register 中使用的路径与最终提交的数据结构完全一致,无需额外转换。
  • 校验也可以针对嵌套字段:register('address.city', { required: '城市不能为空' })
  • 如需设置默认值,在 defaultValues 中按结构传入即可,React Hook Form 会正确展开。

2. 动态表单:使用 useFieldArray 处理数组字段

当表单包含可增删的数组字段时(如添加多个联系人、多个工作经历),useFieldArray 是标准解法。它提供了对数组的增删改插能力,并自动管理每个字段的 key(用于渲染列表)和记录变更。

2.1 基础用法:添加/删除联系人

假设需要一个“紧急联系人”列表,每个联系人包含姓名和电话。

import { useForm, useFieldArray } from 'react-hook-form';

function EmergencyContactsForm() {
  const { register, control, handleSubmit } = useForm({
    defaultValues: {
      contacts: [{ name: '', phone: '' }]
    }
  });

  const { fields, append, remove } = useFieldArray({
    control,          // 必须传入 control
    name: 'contacts'  // 对应表单中的数组字段名
  });

  const onSubmit = (data) => console.log(data.contacts);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {fields.map((field, index) => (
        <div key={field.id}>   {/* 使用 field.id 作为 key,而不是 index */}
          <input
            placeholder="姓名"
            {...register(`contacts.${index}.name`, { required: '必填' })}
          />
          <input
            placeholder="电话"
            {...register(`contacts.${index}.phone`)}
          />
          <button type="button" onClick={() => remove(index)}>
            删除
          </button>
        </div>
      ))}

      <button type="button" onClick={() => append({ name: '', phone: '' })}>
        添加联系人
      </button>
      <button type="submit">提交</button>
    </form>
  );
}

关键点:

  • useFieldArray 返回的 fields 数组中每个元素都包含一个 id 字段,这是 React 要求稳定 key 的保证。不要使用索引作为 key,否则删除/重排时会出现渲染错乱或输入内容残留。
  • 通过 appendremoveinsertswapmove 等方法修改数组,React Hook Form 会自动同步表单状态。
  • 校验规则可以直接写在 register 的第二个参数中,例如 { required: true }

2.2 更复杂的场景:嵌套对象数组

如果数组的每一项本身又是一个对象,而且对象内还有嵌套,路径继续延伸即可。例如工作经历数组,每项包含公司名、起止时间(开始/结束是一个小对象):

defaultValues: {
  experiences: [
    { company: '', period: { start: '', end: '' } }
  ]
}

// 注册:
{...register(`experiences.${index}.company`)}
{...register(`experiences.${index}.period.start`)}
{...register(`experiences.${index}.period.end`)}

useFieldArray 处理这种嵌套也很自然,只关注数组本身即可。

3. 动态表单的性能与最佳实践

3.1 避免不必要的重渲染

当使用 useFieldArray 时,数组字段的变化只会导致当前表单组件的重渲染,不会引起兄弟组件的额外更新。但如果你的动态表单非常庞大(例如一次渲染几十个字段),可以进一步通过 React.memo 包裹每个列表项组件,并配合 useFormContext 来避免传递过多的 register

3.2 与第三方 UI 库集成

useFieldArray 返回的 fields 可以映射到任何 UI 组件。如果你使用 Ant Design、MUI 等组件库,只需将 register 返回的 refonChange 包装一下,或者使用 Controller 组件(React Hook Form 官方推荐的方式)。

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

{fields.map((field, index) => (
  <div key={field.id}>
    <Controller
      name={`contacts.${index}.name`}
      control={control}
      rules={{ required: true }}
      render={({ field }) => <Input {...field} />}
    />
  </div>
))}

3.3 默认值同步与重置

当需要根据已有数据回填动态字段时,直接在 useFormdefaultValues 或使用 reset 方法传入完整数据结构。useFieldArray 会自动根据传入的数组长度渲染对应字段,无需手动调用 append

const { reset } = useForm();

useEffect(() => {
  // 假设从接口获取了数据
  fetchUser().then((user) => {
    reset({
      contacts: user.contacts   // 传入完整数组
    });
  });
}, [reset]);

4. 常见坑点与解决方案

  • 删除后输入框残留值:务必使用 field.id 作为 key,不要用 index。React 会根据 key 判断组件是否复用,key 不变会导致输入状态保留。
  • append/remove 后校验状态不更新:React Hook Form 默认会在字段注册时绑定校验,动态字段增减后,对应的校验会自动添加/移除,无需手动处理。如果发现没触发,检查是否使用了 trigger 手动触发整个表单校验。
  • 数组嵌套数组:不建议过深的嵌套数组,业务上尽可能扁平化。如果确实需要,useFieldArray 可以嵌套使用(需要不同的 name),但复杂度较高,建议重新审视数据结构设计。
  • 处理文件等非字符串值:在数组字段中上传文件,需要结合 Controller 或受控组件,使用 setValue 手动设置文件对象,因为原生的 <input type="file"> 无法通过 ref 直接注册值。

动态表单和数组字段是业务表单中最具挑战的部分,React Hook Form 提供的 useFieldArray 让开发者无需手动处理索引、key、状态同步等底层细节,显著降低了复杂表单的实现成本。只要掌握了路径命名和数组操作方法,就能快速构建出健壮且可维护的复杂表单。