如何在不重新渲染未删除的项目的情况下从列表中删除一个项目?

3
感谢您所有的评论和有益的言论。根据您的建议,请在下面找到一个代码沙盒链接。

https://codesandbox.io/s/magical-butterfly-uk0fjq?file=/src/item.js

这可能有助于您找出问题所在。 在控制台中,多个日志都很明显,考虑到示例的当前规模,这不会造成任何性能问题,但如果有一个长列表,则可能会造成性能问题。

因此,问题是如何防止未被删除的项目重新呈现,尽管父级(ItemList)重新呈现。

控制台显示了Item的渲染。

如先前所述,我使用了useMemo+useCallback组合,但结果证明不稳定。

希望这个示例能够帮助并更加明确。

编辑

关于控制台log,奇怪的是,沙箱示例记录了2次App,2次ItemList和12次Item,而在计算机上只记录了1-1-6次。


1
这就是React的工作原理。状态(State)决定了组件(Component)的渲染方式。如果你改变了状态,组件将会基于新的状态进行重新渲染。那么具体的性能问题是什么? - Andy
1
但是,还有很多你不需要的代码。1)每个项目都需要成为一个表格吗?您不应在th中使用div。3)ItemList应该只是对数据进行map并返回Items数组-不需要useEffect。这些是我简要查看代码时发现的。哦,ItemList不需要自己的状态 - Andy
1
你能简化一下你的例子吗?只保留核心部分。 - pery mimon
首先,阅读 https://reactjs.org/docs/lists-and-keys.html 了解为什么不应该使用数组的 index 作为元素的 key。然后,除非它真正影响了您的性能(也请阅读 https://kentcdodds.com/blog/fix-the-slow-render-before-you-fix-the-re-render),否则不必担心重新渲染。 - Gabriele Petrioli
谢谢@GabrielePetrioli,我已经在沙盒示例中修改了这个问题。 - FJTAIL
显示剩余2条评论
4个回答

3

如果您想最小化项目的重新渲染,需要确保以下几点:

组件记忆化

您可以在所需组件上使用React.memo,这将防止在属性/状态没有改变时重新渲染。

因此,请使用:

export default React.memo(Item);

代替

export default Item;

确保 props 相同

当您渲染 Item 组件时,将 deleteItem 作为 prop 传递,并附带从 App 组件接收的值。然而,此函数不稳定(每次 App 组件重新渲染时都会被重新定义)。由于 App 持有 items 状态,因此每次删除后它都会重新渲染。这将触发定义新的 deleteItem,并导致 Item 重新渲染。

为了使其稳定,您需要做两件事。

  1. 使用 React.useCallback,它在其依赖项保持不变时重复使用相同的函数。
  2. 使用 setItems 的函数形式

所以,不要这样写:

const deleteItem = (newItem) => {
  const newItemList = items.filter(
    (item) => item.reference !== newItem.reference
  );
  setItems(newItemList);
};

使用

const deleteItem = React.useCallback((itemToDelete) => {
  setItems((currentItems) =>
    currentItems.filter((item) => item.reference !== itemToDelete.reference)
  );
}, []);

你的代码中还有一个问题,在你使用.map函数处理数据时,你将每个Item放入数组中并返回该数组,这是不必要的。直接返回<Item ..>即可。
因此,代码应改为:
    {props.data.map((item, index) => {
      const newList = [];
      newList.push(
        <Item
          deleteItem={props.deleteItem}
          key={item.reference}
          item={item}
        ></Item>
      );
      return newList;
    })}

做。
    {props.data.map((item, index) => {
      return (
        <Item
          deleteItem={props.deleteItem}
          key={item.reference}
          item={item}
        ></Item>
      );
    })}

更新了codesandbox,包括所有更改:https://codesandbox.io/s/twilight-water-9iyobx


非常感谢 @Gabriele,我已经测试了它,它完美地运行了,感谢您清晰的解释!我修改了旧版本中使用Memo和useCallback的问题,并解决了这个map函数问题,这实际上是问题的核心。 - FJTAIL

1

看起来你想遍历对象数组,以产生数据表中的行,并使每行的最后一个单元格成为“删除”按钮,以删除该行。这是可以做到的,不需要使用useEffect

const { useState } = React;

// Pass in the data
function Example({ data }) {

  // Set the state with the data
  const [ tableData, setTableData ] = useState(data);

  // When a remove button is clicked `filter`
  // out those rows that don't match the row id
  // and reset the state
  function handleRemove(e) {
    const { id } = e.target.closest('tr').dataset;
    const filtered = tableData.filter(obj => {
      return obj.name !== id;
    });
    setTableData(filtered);
  }

  // Create some rows by mapping over the
  // table data
  return (
    <table>
      {tableData.map(row => {
        return (
          <Row
            key={row.name}
            row={row}
            handleRemove={handleRemove}
          />
        );
      })}
    </table>
  );

}

// Create a table row and populate the cells
// with the information from the row object
function Row({ row, handleRemove }) {
  return (
    <tr data-id={row.name}>
      <td>{row.name}</td>
      <td>{row.reference}</td>
      <td>{row.description}</td>
      <td>
        <button
          onClick={handleRemove}
        >Remove
        </button>
      </td>
    </tr>
  );
}

const data = [
 {name: "NAME 1", reference: "REF 1", description: "LOREM IPSUM"},
 {name: "NAME 2", reference: "REF 2", description: "LOREM IPSUM"},
 {name: "NAME 3", reference: "REF 3", description: "LOREM IPSUM"},
 {name: "NAME 4", reference: "REF 4", description: "LOREM IPSUM"},
 {name: "NAME 5", reference: "REF 5", description: "LOREM IPSUM"}
];

ReactDOM.render(
  <Example data={data} />,
  document.getElementById('react')
);
table { border-collapse: collapse; border: 1px solid #565656; }
td { border: 1px solid #dfdfdf; padding: 0.5em;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>


谢谢Andy提供的示例,我已经尝试过了,确实你的代码更简单。不过,我插入了各种控制台日志来跟踪重新渲染,每个项目在按下REMOVE按钮时都会记录日志。事实上,这就是问题所在,我承认一开始并不清楚。 - FJTAIL

-1

在更新状态时,您需要先展开先前的状态,然后再进行更新。 例如:

const deleteItem = (newItem) => {
  const newItemList = items.filter(item => item.reference !== newItem.reference);
  setItems(prev => (...prev, newItemList));
}

这是一个删除操作。没有什么需要扩散的,filter 就可以了。不过参数的名称应该更好一些,因为 newItem 很容易引起误解。 - Gabriele Petrioli

-1

在更新状态时,您需要先展开先前的状态,然后再进行更新。

根据您的类型,您必须返回数组或对象 展开运算符打开了先前的数组或对象,然后您添加了新数据 setItems(prev => [...prev, newItemList])


这是一个删除操作。没有什么需要扩散的,filter 很好。不过参数名称可以更好,因为 newItem 有误导性。 - Gabriele Petrioli
你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心找到有关如何编写良好答案的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接