在实际业务中,表单很少只是扁平的一层字段。常见的中等复杂度场景包括:嵌套对象(如收货地址包含省/市/详细地址)、动态列表(如添加多个团队成员、多个发票抬头)、以及可增删的数组字段。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,否则删除/重排时会出现渲染错乱或输入内容残留。- 通过
append、remove、insert、swap、move等方法修改数组,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 返回的 ref 和 onChange 包装一下,或者使用 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 默认值同步与重置
当需要根据已有数据回填动态字段时,直接在 useForm 的 defaultValues 或使用 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、状态同步等底层细节,显著降低了复杂表单的实现成本。只要掌握了路径命名和数组操作方法,就能快速构建出健壮且可维护的复杂表单。