【问题标题】:How to avoid rerender all child components which in loop when parent component state update如何避免在父组件状态更新时重新渲染循环中的所有子组件
【发布时间】:2021-04-14 05:34:49
【问题描述】:

我有一个子组件,它位于父组件的循环中。当其中一个子组件正在更新父组件的状态时,它会重新渲染所有子组件,因为它是循环的。如何避免每次迭代的重新渲染。


function Parent() {
  const [selectedChild, setSelectedChild] = useState([]);

  const onChangeHandle = (event, id) => {
    const checked = event.target.checked;
      let updatedArray = [...selectedChild];
      if(checked){
         if(!selectedChild.includes(id)){
            updatedArray.push(id); 
         }
      }
      else{
         var index = updatedArray.indexOf(id)
         if (index !== -1) {
            updatedArray.splice(index, 1);
         }
      }
      setSelectedChild(updatedArray);
  }
  const dummy = (id) => {
    return selectedChild.includes(id);
  }
  return (
    <div>
    <table>
    <tbody>
      {[1,2,3].map((value, index) => {
      return (
        <Child 
        key={index} 
        index={index} 
        value={value} 
        handle={onChangeHandle}
        isSelected={dummy}
        />
      )
      })}
    </tbody>
    </table>
    <div>
      {selectedChild}
    </div>
  </div>)
}

function Child({index, value, handle, isSelected }) {
  console.log('rendering')

 return (
 <tr>
    <td>
      <input 
      type="checkbox" 
      checked={isSelected(index)}
      onChange={(event) => handle(event, index)}/>
    </td>
    <td>hello {index} {value}</td>
 </tr>
 )
}

export default function App() {
  return (
    <div className="App">
      <Parent />
    </div>
  );
}

当前行为: 在上面的代码中,当我单击其中一个子组件中的复选框时,它正在更新父组件状态(selectedChild)。所以循环正在执行,所有子项(所有表行)都在重新渲染。

预期行为: 只有特定的行需要重新渲染

演示: https://codesandbox.io/s/newpro-0pezc

【问题讨论】:

标签: javascript reactjs react-hooks react-usememo


【解决方案1】:

为此,您可以使用React.memo,如果道具保持不变,它将记住您的组件。但是鉴于您的代码,您需要进行一些额外的更改:

  • 你必须申请useCallback来记忆onChangeHandle函数;

  • 要正确记忆 onChangeHandle,您需要对其进行重构。你不能直接传递selectedChild,否则它会记住它的值。使用 setSelectedChild 作为参数传递一个采用 selectedChild 的函数。

  • 您的孩子应该收到 isSelected 作为布尔值而不是函数。否则 props 将保持不变,并且 Child 永远不会更新;

    import React, { useState, memo, useCallback } from "react";
    
    function Parent() {
      const [selectedChild, setSelectedChild] = useState([]);
    
      const onChangeHandle = useCallback((event, id) => {
        setSelectedChild(selectedChild => {
          const checked = event.target.checked;
          let updatedArray = [...selectedChild];
          if (checked) {
            if (!selectedChild.includes(id)) {
              updatedArray.push(id);
            }
          } else {
            var index = updatedArray.indexOf(id);
            if (index !== -1) {
              updatedArray.splice(index, 1);
            }
          }
          return updatedArray;
        });
      }, []);
    
      const dummy = id => {
        return selectedChild.includes(id);
      };
    
      const renderChildren = () =>
        [1, 2, 3].map((value, index) => {
          return (
            <Child
              key={index}
              index={index}
              value={value}
              handle={onChangeHandle}
              isSelected={dummy(index)}
            />
          );
        });
    
      return (
        <div>
          <table>
            <tbody>{renderChildren()}</tbody>
          </table>
          <div>{selectedChild}</div>
        </div>
      );
    }
    
    const Child = memo(({ index, value, handle, isSelected }) => {
      console.log("rendering");
    
      return (
        <tr>
          <td>
            <input
              type="checkbox"
              checked={isSelected}
              onChange={event => handle(event, index)}
            />
          </td>
          <td>
            hello {index} {value}
          </td>
        </tr>
      );
    });
    
    export default function App() {
      return (
        <div className="App">
          <Parent />
        </div>
      );
    }
    

https://stackblitz.com/edit/so-memo-children?file=src/App.js

【讨论】:

  • setter 回调的有趣用法。 +1
  • 顺便说一句:函数有参数,但你将参数传递给函数,而不是参数。调用函数时,将传递的参数分配给参数。
  • @buzatto - 你的回答帮助了我。但是我的用例几乎没有变化。你能看到这个stackoverflow.com/questions/66046375/…
【解决方案2】:

基本答案是在Child 上使用React.memo

const Child = memo(function Child(...) {...})

但是要使memo 工作,如果组件不应该重新渲染,它需要接收相同的道具。这意味着在onChangeHandle 上使用useCallback

 const onChangeHandle = useCallback((event, id) => {...}, [])

但由于onChangeHandle 使用selectedChild 总是在复选框更改时更改,您还需要使用useRef 引用它:

const selectedChildRef = useRef();
selectedChildRef.current = selectedChild;

并在 onChangeHandle 中使用 refed 版本。

需要做的最后一件事是将isSelected prop 从函数更改为只是一个标志,因为它需要在每次复选框更改时运行:

isSelected={selectedChild.includes(index)}

https://codesandbox.io/s/newpro-forked-wxvqs

【讨论】:

  • 乍一看,我没有想到 useRef,这是一个可靠的方法。
  • @buzatto:我观察到使用 useRef() 实际上不会在 React Usecallback 中引发依赖错误,请您指导我为什么会这样?我问的原因是,在我的实现中,我不需要依赖 refs,只是为了解决我正在使用的依赖错误。这是我的 POC 链接:link
  • 与setter类似,refsstableuseRef的返回值始终是同一个对象)所以你不必将它们放在deps中而ref.current 将始终存储最新值。
  • 你可以使用 setState 更新函数而不是依赖ref
【解决方案3】:

您可以在 Child 的定义中实现 shouldComponentUpdate (doc: https://reactjs.org/docs/react-component.html#shouldcomponentupdate) 以更好地控制何时重新呈现。但这仅适用于您遇到性能问题的情况 - 通常您不必担心它,让它们全部重新呈现是标准的。

【讨论】:

    猜你喜欢
    • 2022-06-29
    • 1970-01-01
    • 2020-05-04
    • 2021-04-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-08-14
    相关资源
    最近更新 更多