React - 如何在一个对象数组中只重新渲染一个组件,而不是全部重新渲染

4
我有一个简单的问题与React渲染有关。在我解释之前,这是代码沙箱的链接:https://codesandbox.io/s/list-rerendering-y3iust?file = / src / App.js 事实是,我有一个存储在父组件App中的对象数组。每个对象都有一个“checked”字段,我希望在单击该对象的相应复选框时切换它。我循环遍历对象数组并在Child组件中显示它们。当我单击复选框时,handleChange函数执行,并且列表在父组件中更新,导致App重新呈现以及所有 Child组件。 我想要解决的问题是,如何使其仅重新呈现被单击的Child组件而不是所有Child组件? 我尝试使用useCallback和列表状态的功能性更新,但这没有做任何事情,它仍然重新呈现所有子组件而不是正在切换的子组件。我有一种感觉,我使用useCallback不正确,并且正在创建全新的函数。我想了解React在处理数组时如何重新呈现,将先前的数组与新数组进行比较的解释。我理解,在我的代码中,我通过解构将其提供给原始列表的副本,然后将其放入新数组中,这显然不是对原始列表的引用,因此React将副本设置为新状态:
import { useCallback, useState } from "react";
import Child from "./Child";
import "./styles.css";

const mockList = [
  { text: "1", id: 1, checked: false },
  { text: "2", id: 2, checked: false },
  { text: "3", id: 3, checked: false },
  { text: "4", id: 4, checked: false },
  { text: "5", id: 5, checked: false }
];

export default function App() {
  const [list, setList] = useState(mockList);

  const handleChange = useCallback((checked, id) => {
    setList((oldList) => {
      for (let i = 0; i < oldList.length; i++) {
        if (oldList[i].id === id) {
          oldList[i].checked = checked;
          break;
        }
      }
      return [...oldList];
    });
  }, []);

  return (
    <div className="App">
      {list.map((item) => (
        <Child
          key={item.id}
          text={item.text}
          checked={item.checked}
          handleChange={(checked) => handleChange(checked, item.id)}
        />
      ))}
    </div>
  );
}

Child.js

const Child = ({ text, checked, handleChange }) => {
  console.log("Child rerender");
  return (
    <div
      style={{
        display: "flex",
        border: "1px solid green",
        justifyContent: "space-around"
      }}
    >
      <p>{text}</p>
      <input
        style={{ width: "20rem" }}
        type="checkbox"
        checked={checked}
        onChange={(e) => handleChange(e.checked)}
      />
    </div>
  );
};

export default Child;


你可以使用 React.memo()Child 组件包装起来,使其成为一个纯组件,并在导出时进行包装。 - Irfanullah Jan
2个回答

3
这是优化的方法,首先你错误地使用了useCallback,因为每次重新渲染时,(e) => handleChange(e.checked)都是一个新的实例,因此即使我们记忆了Child,它仍然会重新渲染,因为props总是新的。所以我们需要用useCallback来调用handleChange函数,请参考我的forked codesandbox。 https://codesandbox.io/s/list-rerendering-forked-tlkwgh?file=/src/App.js

啊,谢谢你指出来 :) - Irfanullah Jan

2

React与您如何操作数组或对象无关。它只是继续渲染您的应用程序树,并且有一些规则来决定何时停止重新呈现树中的某个分支。渲染只是意味着React调用组件函数,这些函数本身返回子树或节点。请参见https://reactjs.org/docs/reconciliation.html

尝试使用React.memo()包装Child

const Child = ({ text, checked, handleChange }) => {
  console.log("Child rerender");
  return (
    <div
      style={{
        display: "flex",
        border: "1px solid green",
        justifyContent: "space-around"
      }}
    >
      <p>{text}</p>
      <input
        style={{ width: "20rem" }}
        type="checkbox"
        checked={checked}
        onChange={(e) => handleChange(e.checked)}
      />
    </div>
  );
};

export default React.memo(Child);

React.memo() 的作用是比较前一个属性和下一个属性,仅在任何道具发生更改时重新渲染Child组件,并且如果属性保持不变,则不会重新渲染。

顺便说一下:请阅读此https://kentcdodds.com/blog/usememo-and-usecallback
简而言之:不建议以代码复杂性为代价过度优化。预防“不必要”的渲染可能会带来额外的内存使用和计算成本:因此,只有在非常确定它将在特定问题上有所帮助时才需要这样做。


感谢您的建议和附注!我在一个非常大的项目中遇到了这个问题,重新渲染非常昂贵,会严重拖慢我的应用程序,因此我想提供一个不太复杂但适用于我的项目的示例。 - AdrianRonquillo
我本来期望React在默认情况下会在渲染之前比较props。现在我知道了React.memo,这对我的代码产生了巨大的影响。非常感谢! - Jose
是的,在计算能力较慢的组件中,React.memo()非常有用。最近,与useDeferredValue和useTransition一起使用它非常方便。@Jose - Irfanullah Jan

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