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

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会自动同步对应字段的数组值:

  1. 父组件用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>
  )
}
  1. 子组件直接调用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.02 23:33:29