React Hook Form useFieldArray编辑模式append重复追加表单项问题
问题根因
你遇到的重复渲染表单项问题,是几个问题叠加导致的:
- React 18 开发环境默认开启严格模式,会对未做清理防护的
useEffect执行两次,用来检测不规范的副作用写法。你在useEffect里遍历调用append没有加任何执行判断,第一次执行追加1条数据,第二次重跑副作用又追加1条,直接出现重复项。如果传入的是N条数据,最终会渲染2N条重复项。 - 动态表单项初始化逻辑不符合React Hook Form的设计:编辑场景下不需要手动遍历
append塞初始值,这种写法本身就容易因为重渲染、依赖变化导致重复执行。 - 大概率你的
useEffect依赖项配置不严谨,只要父组件重渲染导致传入的bookingLineDtos引用变化,就算数据内容完全一致,也会重新触发追加逻辑。 - 额外的性能问题:循环调用
append会触发多次重渲染,本身就是useFieldArray的不推荐用法。
修复方案
方案1:官方推荐写法(彻底规避问题,无额外副作用)
不要靠子组件useEffect追加初始值,直接在表单初始化阶段注入编辑数据,useFieldArray会自动同步对应字段的数组值:
- 父组件用
useForm初始化表单时,给动态表单项设置空数组默认值,接口拿到编辑数据后调用reset方法一次性注入整个表单值,不需要把bookingLines单独当props传给子组件。
示例代码:
// 父组件 BookingTest import { useForm, FormProvider } from 'react-hook-form'; import BookingLine from './BookingLine'; export default function BookingTest() { const methods = useForm({ defaultValues: { // 其他普通表单字段的默认值 bookingId: '', bookingLines: [] // 动态行默认空数组 } }); const { reset } = methods; useEffect(() => { const getDetail = async () => { // 替换成你的接口请求逻辑 const res = await fetch(`/你的预订详情接口/${当前编辑的预订ID}`); const detail = await res.json(); // 接口返回数据后直接重置表单,所有字段包括嵌套的bookingLines都会自动赋值 reset(detail); }; getDetail(); }, [reset]); return ( <FormProvider {...methods}> {/* 其他普通表单项 */} <BookingLine /> </FormProvider> ) }
- 子组件直接调用
useFieldArray读取字段列表即可,删掉原来的useEffect追加逻辑:
// 子组件 BookingLine import { useFormContext, useFieldArray } from 'react-hook-form'; export default function BookingLine() { const { control, register } = useFormContext(); const { fields, append, remove } = useFieldArray({ control, name: 'bookingLines' // 和defaultValues里的字段名保持一致 }); return ( <div className="booking-line-wrapper"> {fields.map((field, index) => ( <div key={field.id} className="line-item"> {/* 替换成你实际的表单项 */} <input {...register(`bookingLines.${index}.skuName`)} placeholder="商品名称" /> <input {...register(`bookingLines.${index}.count`)} type="number" placeholder="数量" /> <button onClick={() => remove(index)}>删除该行</button> </div> ))} <button onClick={() => append({ skuName: '', count: 1 })}>新增预订行</button> </div> ) }
这种写法完全不需要手动处理初始值追加,不管是新增还是编辑场景逻辑统一,不会出现重复追加的问题,性能也更好。
方案2:保留现有props传值逻辑的兜底修复
如果你因为业务逻辑限制,必须保留父组件传bookingLineDtos给子组件、子组件初始化追加的写法,需要做两层防护:
- 用
useRef做初始化标记,保证同一份编辑数据只执行一次注入 - 不要循环调用
append,用useFieldArray提供的replace方法一次性替换整个数组,避免多次重渲染
示例代码:
// 子组件 BookingLine import { useRef } from 'react'; import { useFormContext, useFieldArray } from 'react-hook-form'; export default function BookingLine({ bookingLineDtos }) { const { control, register } = useFormContext(); const { fields, append, remove, replace } = useFieldArray({ control, name: 'bookingLines' }); // 初始化标记 const hasInit = useRef(false); useEffect(() => { // 已经初始化过、或者传入的数组为空时直接跳过 if (hasInit.current || !Array.isArray(bookingLineDtos) || !bookingLineDtos.length) return; // 一次性替换整个动态表单项数组 replace(bookingLineDtos); hasInit.current = true; // 组件卸载时重置标记,避免切换不同编辑ID时无法更新 return () => { hasInit.current = false; } }, [bookingLineDtos, replace]); // 下面的渲染逻辑和上面一致 return ( // 你的渲染代码 ) }
额外注意事项
- 不要把
fields、fields.length加入上述初始化useEffect的依赖项,否则每次追加完字段数组变化,会重新触发副作用导致死循环。 - 如果不需要兼容React17及以下版本,不要关闭严格模式,严格模式的重复执行是帮你提前发现不规范的副作用写法,不是bug。
内容的提问来源于stack exchange,提问作者Workaholic
相关产品推荐
相关产品推荐

