使用 JSX 的地图时如何避免子组件被卸载?

2024-01-06

这是一个更简洁的版本我之前提出的一个问题 https://stackoverflow.com/questions/69240704/rendering-of-children-components-triggers-unwanted-unmounts。希望它能得到更好的解释和更容易理解。

这是一个小应用程序,有 3 个需要输入数字的输入(请忽略您也可以输入非数字,这不是重点)。它计算所有显示数字的总和。如果您将其中一个输入更改为另一个数字,则总和会更新。

这是它的代码:

import { useCallback, useEffect, useState } from 'react';

function App() {

  const [items, setItems] = useState([
    { index: 0, value: "1" },
    { index: 1, value: "2" },
    { index: 2, value: "3" },
  ]);

  const callback = useCallback((item) => {
    let newItems = [...items];
    newItems[item.index] = item;
    setItems(newItems);
  }, [items]);

  return (
    <div>
      <SumItems items={items} />
      <ul>
        {items.map((item) =>
          <ListItem key={item.index} item={item} callback={callback} />
        )}
      </ul>
    </div>
  );
}


function ListItem(props) {

  const [item, setItem] = useState(props.item);

  useEffect(() => {
    console.log("ListItem ", item.index, " mounting");
  })

  useEffect(() => {
    return () => console.log("ListItem ", item.index, " unmounting");
  });

  useEffect(() => {
    console.log("ListItem ", item.index, " updated");
  }, [item]);

  const onInputChange = (event) => {
    const newItem = { ...item, value: event.target.value };
    setItem(newItem);
    props.callback(newItem);
  }

  return (
    <div>
      <input type="text" value={item.value} onChange={onInputChange} />
    </div>);
};

function SumItems(props) {
  return (
    <div>Sum : {props.items.reduce((total, item) => total + parseInt(item.value), 0)}</div>
  )

}

export default App;

这是启动时以及将第二个输入 2 更改为 4 后的控制台输出:

ListItem  0  mounting App.js:35
ListItem  0  updated App.js:43
ListItem  1  mounting App.js:35
ListItem  1  updated App.js:43
ListItem  2  mounting App.js:35
ListItem  2  updated App.js:43
ListItem  0  unmounting react_devtools_backend.js:4049:25
ListItem  1  unmounting react_devtools_backend.js:4049:25
ListItem  2  unmounting react_devtools_backend.js:4049:25
ListItem  0  mounting react_devtools_backend.js:4049:25
ListItem  1  mounting react_devtools_backend.js:4049:25
ListItem  1  updated react_devtools_backend.js:4049:25
ListItem  2  mounting react_devtools_backend.js:4049:25

正如您所看到的,当更新单个输入时,所有子级都不会重新渲染,它们首先被卸载,然后重新安装。太浪费了,所有的输入都已经处于正确的状态,只需要更新总和。想象一下有数百个这样的输入。

如果这只是重新渲染的问题,我可以考虑记忆。但这是行不通的,因为callback更新正是因为items改变。不,我的问题是关于卸载all这些孩子。

问题1: 可以避免卸载吗?

如果我相信肯特·C·多兹 (Kent C. Dodds) 撰写的这篇文章 https://kentcdodds.com/blog/understanding-reacts-key-prop,答案是否定的(强调我的):

React 的 key prop 使您能够控制组件实例。 每次 React 渲染你的组件时,它都会调用你的函数 检索用于更新 DOM 的新 React 元素。如果 你返回相同的元素类型,它保留那些组件/DOM 节点 周围,​​即使所有*道具都改变了。

(...)

例外的是关键道具。这允许您返回 完全相同的元素类型,但是强制 React 卸载之前的 实例,然后安装一个新的。这意味着所有拥有 当时存在于组件中的内容被完全删除,并且 出于所有意图和目的,组件被“重新初始化”。

问题2:如果这是真的,那么我应该考虑什么设计来避免看似不必要的事情并导致问题在我真实的 https://stackoverflow.com/questions/69240704/rendering-of-children-components-triggers-unwanted-unmountsapp 是因为每个输入组件中都发生异步处理?


正如您所看到的,当更新单个输入时,所有子项都会更新 不重新渲染,它们首先被卸载,然后重新安装。真是个 浪费,所有输入都已经处于正确状态,只有总和 需要更新。想象一下有数百个这样的输入。

不,您从以下位置看到的日志useEffect不代表组件安装/卸载。您可以检查 DOM 并验证即使所有三个组件都重新渲染,也只有一个输入被更新。

如果这只是重新渲染的问题,我可以考虑记忆。 但这是行不通的,因为回调的更新正是因为 项目发生变化。不,我的问题是关于卸载所有 孩子们。

您可以在此处使用功能状态更新来访问先前的状态并返回新的状态。

const callback = useCallback((item) => {
    setItems((prevItems) =>
      Object.assign([...prevItems], { [item.index]: item })
    );
}, []);

现在,您可以使用React.memo因为回调不会改变。这是更新后的演示:

正如您所看到的,当其中之一发生更改时,仅记录相应的输入日志,而不是全部三个。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用 JSX 的地图时如何避免子组件被卸载? 的相关文章