繁体   English   中英

如果 function 更新 state 变量,该变量是 object 并从子组件调用,那么避免无限渲染的最佳方法是什么?

[英]If a function updates a state variable which is an object and is called from a child component, what's the best way to avoid infinite renders?

我在 React 上做一些编码,遇到了一个我想妥善处理的问题。 下文提供了有关此事的详细信息。

环境

假设您有一个组件FormDemo来处理可能很复杂的表单,其中部分涉及某些输入字段的动态管理。 例如,由于 JavaScript 对数组长度的限制,所提供的代码示例允许为 0 和 (2 32 - 1) 之间的字段创建任意数量的字段。

按所有名称字段上方的添加新名称按钮到 append 另一个名称字段。 按任何输入右侧的删除按钮将其从列表中删除。

创建的每个名称输入都由一个单独的组件SubForm处理,该组件具有三个属性:

  • id :当前字段的唯一生成标识符。
  • onChange :只要输入的值发生更改,就会执行 function。
  • onRemove :只要单击该表单的“删除”按钮,就会执行 function。

样本

这是我在 CodeSandbox 上制作的代码的工作示例,用于演示目的。

问题

代码示例中使用的方法有效,但它有 CodeSandbox 的 Problems 选项卡中提到的 eslint 问题,我知道这不是 CodeSandbox 问题,因为我在本地环境中测试了同一个项目并遇到了同样的问题. 以下是直接从控制台获取的问题详细信息:

React Hook useEffect 缺少依赖项:'onChange'。 包括它或删除依赖数组。 如果 'onChange' 更改过于频繁,请找到定义它的父组件并将该定义包装在 useCallback 中。 (react-hooks/exhaustive-deps)

直接遵循问题的建议(即添加onChangeSubForm的 useEffect 的依赖项列表)会导致无限渲染,因此不是问题的解决方案。

这项研究

在阅读了一些关于 useCallback 的官方 React文档以及关于 useEffect的其他部分之后,我发现在渲染组件时,React 会创建在组件主体中声明的函数的新实例。 因此,将此类函数添加到某个useEffect挂钩的依赖项列表中,该挂钩具有附加到它的效果 function 将需要在每个渲染上调用 function。

在我的方法中,我将update function 传递给onChange属性中的SubForm组件作为参考( 此处由 React 文档证明),因此SubForm组件的onChange属性具有与父组件完全相同的update function 实例。 因此,每当update function 的实例发生变化时,它添加到useEffect挂钩的依赖项中,该挂钩执行附加到它的效果 function ,并且考虑到上述情况,这发生在父组件FormDemo的每次渲染上。

update function 更改了FormDemo组件的 state 变量forms的值,导致它重新呈现。 这将重新创建update function 的实例。 SubForm组件收到该更改的通知并执行附加到useEffect挂钩的效果 function,再次调用update function。 反过来,这会导致 state 变量forms的另一个变化,告诉父组件FormDemo再次渲染......这会无限期地继续,创建一个无限循环的渲染。

有些人可能会问,如果表单的输入字段在这两个渲染之间没有更改,为什么会发生这种情况,因此传递给update function 的值实际上与以前相同。 经过一些测试,你可以 在这里自己尝试,我得出的结论是它实际上是错误的:设置为forms的值总是不同的。 这是因为即使对象的内容完全相同,但它的实例是不同的,React 比较的是 object 实例而不是它们的内容,如果这些实例不同,则发送一个重新渲染组件的命令。

As useCallback hook memoizes the instance of the function between renders, recreating the function only when the values or instances of its dependencies change, I've assumed that wrapping update function in that hook will solve the original problem, because the instance of the function will始终保持不变。

但是,在 useCallback 中包装update useCallback会导致另一个问题:我需要添加forms作为依赖项,因为我在 ZC1C425268E68385D1AB5074C17A94F14 中使用它。 但是,考虑到上述情况,由于forms的实例在每次更新后都不同,这将使我回到原来的问题,这将命令useCallback重新创建 function 的实例。

潜在解决方案

尽管如此,我有一个我不太喜欢的解决方案,即使它有效,因为它不需要将 state 变量forms添加到useCallback的依赖项列表中:

const update = useCallback((id, value) => {
  setForms(prevState => {
    const { form_list } = prevState,
          new_forms     = [...form_list],
          mod_id        = new_forms.map((e) => e.id).indexOf(id);

    new_forms[mod_id] = value;

    return { ...prevState, form_list: new_forms };
  });
}, []);

那么我为什么反对它,如果它有效并且在控制台中没有问题呢?

以我的拙见(请随意证明我错了),因为这些问题:

  1. 直接使用 state setter function 代替专用中间件 function。 这分散了直接 state 管理。
  2. 复制原始数组,如果数组内部有很多值,在 memory 上可能会很昂贵,更不用说每个值本身都是 object。

问题

在提供的将使用中间件 function setField的情况下,对所述问题的最节省内存和可读性的解决方案是什么? 或者,如果可以用潜在的解决方案来揭穿我的问题,请证明这是 go 的最佳方法。

如果需要解决方案,请随意修改setField的内容,并记住我愿意回答与问题相关的任何内容。

您可能希望将 ID 的管理与 SubFrom 分开。 SubForm 不应该改变它的 ID。

  1. 包装updateremove功能 - 所以 SubForm 不需要发回 ID。
  <SubForm key={e.id} id={e.id} 
          onChange={(form) => update(e.id, form)} 
          onRemove={() => remove(e.id) } />
  1. 确保 SubForm 不会更改 ID 作为form的一部分
  const update = (id, value) => {
      setField(
        "form_list", 
        // subform shouldn't change id, so we overriding it (to be sure)
        forms.form_list.map(e => e.id===id?{...value, id}:e)
      );
  };

您仍然可以选择将 ID 传递给 SubForm,但 ID 的管理与它是分开的。

修改后的代码

看来您正在复制每个 SubForm 的SubForm :您将其存储在 parent 和SubForm中,为什么不将 state 仅存储在 parent 中并作为道具传递?

我在谈论这样的事情:

const SubForm = ({ id, form, onChange, onRemove }) => {
  return (
    <Form>
      <Form.Group controlId={`form_text${id}`}>
        <Form.Label>Name (ID {id})</Form.Label>
        <InputGroup>
          <Form.Control
            type="text"
            value={form.name}
            onChange={(e) => onChange(id, { ...form, name: e.target.value })}
          />
          <Button variant="danger" onClick={() => onRemove(id)}>
            Remove
          </Button>
        </InputGroup>
      </Form.Group>
      <br />
      <br />
    </Form>
  );
};

要传递每个表单数据,只需执行以下操作:

<SubForm key={e.id} form={e} id={e.id} onChange={update} onRemove={remove} />

不再需要useEffect了。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM