当前位置: 首页 > 面试题库 >

React Hooks useCallback导致子项重新渲染

叶淇
2023-03-14
问题内容

我正在尝试使用新的Hooks从类组件转变为功能组件。但是,感觉与useCallback类组件中的类函数不同,我将获得不必要的子代渲染。

下面有两个相对简单的片段。第一个是我的示例编写为类,第二个是我的示例重写为功能组件。目的是使功能组件获得与类组件相同的行为。

类组件测试用例

class Block extends React.PureComponent {

  render() {

    console.log("Rendering block: ", this.props.color);



    return (

        <div onClick={this.props.onBlockClick}

          style = {

            {

              width: '200px',

              height: '100px',

              marginTop: '12px',

              backgroundColor: this.props.color,

              textAlign: 'center'

            }

          }>

          {this.props.text}

         </div>

    );

  }

};



class Example extends React.Component {

  state = {

    count: 0

  }





  onClick = () => {

    console.log("I've been clicked when count was: ", this.state.count);

  }



  updateCount = () => {

    this.setState({ count: this.state.count + 1});

  };



  render() {

    console.log("Rendering Example. Count: ", this.state.count);



    return (

      <div style={{ display: 'flex', 'flexDirection': 'row'}}>

        <Block onBlockClick={this.onClick} text={'Click me to log the count!'} color={'orange'}/>

        <Block onBlockClick={this.updateCount} text={'Click me to add to the count'} color={'red'}/>

      </div>

    );

  }

};



ReactDOM.render(<Example/>, document.getElementById('root'));


<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>



<div id='root' style='width: 100%; height: 100%'>

</div>

功能组件测试用例

const Block = React.memo((props) => {

  console.log("Rendering block: ", props.color);



  return (

      <div onClick={props.onBlockClick}

        style = {

          {

            width: '200px',

            height: '100px',

            marginTop: '12px',

            backgroundColor: props.color,

            textAlign: 'center'

          }

        }>

        {props.text}

       </div>

  );

});



const Example = () => {

  const [ count, setCount ] = React.useState(0);

  console.log("Rendering Example. Count: ", count);



  const onClickWithout = React.useCallback(() => {

    console.log("I've been clicked when count was: ", count);

  }, []);



  const onClickWith = React.useCallback(() => {

    console.log("I've been clicked when count was: ", count);

  }, [ count ]);



  const updateCount = React.useCallback(() => {

    setCount(count + 1);

  }, [ count ]);



  return (

    <div style={{ display: 'flex', 'flexDirection': 'row'}}>

      <Block onBlockClick={onClickWithout} text={'Click me to log with empty array as input'} color={'orange'}/>

      <Block onBlockClick={onClickWith} text={'Click me to log with count as input'} color={'cyan'}/>

      <Block onBlockClick={updateCount} text={'Click me to add to the count'} color={'red'}/>

    </div>

  );

};



ReactDOM.render(<Example/>, document.getElementById('root'));


<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>



<div id='root' style='width: 100%; height: 100%'>

</div>

在第一个(类组件)中,我可以通过红色块更新计数,而无需重新渲染任何一个块,并且我可以通过橙色块自由地控制台记录当前计数。

在第二个(功能组件)中,通过红色块更新计数将触发红色和青色块的重新渲染。这是因为,useCallback由于计数已更改,因此将创建其功能的新实例,从而导致块获得新的onClick道具并因此重新渲染。橙色块不会重新渲染,因为useCallback用于橙色的块onClick与计数值无关。这样做会很好,但是当您单击橙色块时,它不会显示计数的实际值。

我以为useCallback这样做的目的是让子代不会获得相同功能的新实例并且不必进行不必要的重新渲染,但这似乎在第二种情况下都会发生,即回调函数使用单个变量,如果并非总是根据我的经验。

那么,如何onClick在不重新渲染子级的情况下在功能组件中实现此功能呢?有可能吗?

更新(解决方案): 使用下面的Ryan Cogswell的答案,我精心制作了一个自定义钩子,以使创建类函数变得容易。

const useMemoizedCallback = (callback, inputs = []) => {
    // Instance var to hold the actual callback.
    const callbackRef = React.useRef(callback);

    // The memoized callback that won't change and calls the changed callbackRef.
    const memoizedCallback = React.useCallback((...args) => {
      return callbackRef.current(...args);
    }, []);

    // The callback that is constantly updated according to the inputs.
    const updatedCallback = React.useCallback(callback, inputs);

    // The effect updates the callbackRef depending on the inputs.
    React.useEffect(() => {
        callbackRef.current = updatedCallback;
    }, inputs);

    // Return the memoized callback.
    return memoizedCallback;
};

然后,我可以像这样轻松地在函数组件中使用它,只需将onClick传递给孩子即可。它将不再重新渲染子级,但仍使用更新的var。

const onClick = useMemoizedCallback(() => {
    console.log("NEW I've been clicked when count was: ", count);
}, [count]);

问题答案:

useCallback会避免由于父级中发生的某些更改(而 不是
回调的依赖项)而导致不必要的子级重新渲染。为了避免在涉及回调的依赖项时重新渲染子项,您需要使用ref。引用是等效于实例变量的钩子。

在下面,我onClickMemoized使用onClickRefwhich指向当前onClick(通过设置useEffect),从而将其委托给知道状态当前值的函数版本。

我还更改updateCount为使用功能性更新语法,因此它不需要依赖count

const Block = React.memo(props => {

  console.log("Rendering block: ", props.color);



  return (

    <div

      onClick={props.onBlockClick}

      style={{

        width: "200px",

        height: "100px",

        marginTop: "12px",

        backgroundColor: props.color,

        textAlign: "center"

      }}

    >

      {props.text}

    </div>

  );

});



const Example = () => {

  const [count, setCount] = React.useState(0);

  console.log("Rendering Example. Count: ", count);



  const onClick = () => {

    console.log("I've been clicked when count was: ", count);

  };

  const onClickRef = React.useRef(onClick);

  React.useEffect(

    () => {

      onClickRef.current = onClick;

    }

  );



  const onClickMemoized = React.useCallback(() => {

    onClickRef.current();

  }, []);



  const updateCount = React.useCallback(() => {

    setCount(prevCount => prevCount + 1);

  }, []);



  return (

    <div style={{ display: "flex", flexDirection: "row" }}>

      <Block

        onBlockClick={onClickMemoized}

        text={"Click me to log with empty array as input"}

        color={"orange"}

      />

      <Block

        onBlockClick={updateCount}

        text={"Click me to add to the count"}

        color={"red"}

      />

    </div>

  );

};



ReactDOM.render(<Example />, document.getElementById("root"));


<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>



<div id='root' style='width: 100%; height: 100%'>

</div>

而且,钩子的优点当然在于您可以将这种有状态逻辑分解为自定义钩子:

import React from "react";
import ReactDOM from "react-dom";

const Block = React.memo(props => {
  console.log("Rendering block: ", props.color);

  return (
    <div
      onClick={props.onBlockClick}
      style={{
        width: "200px",
        height: "100px",
        marginTop: "12px",
        backgroundColor: props.color,
        textAlign: "center"
      }}
    >
      {props.text}
    </div>
  );
});

const useCount = () => {
  const [count, setCount] = React.useState(0);

  const logCount = () => {
    console.log("I've been clicked when count was: ", count);
  };
  const logCountRef = React.useRef(logCount);
  React.useEffect(() => {
    logCountRef.current = logCount;
  });

  const logCountMemoized = React.useCallback(() => {
    logCountRef.current();
  }, []);

  const updateCount = React.useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []);
  return { count, logCount: logCountMemoized, updateCount };
};
const Example = () => {
  const { count, logCount, updateCount } = useCount();
  console.log("Rendering Example. Count: ", count);

  return (
    <div style={{ display: "flex", flexDirection: "row" }}>
      <Block
        onBlockClick={logCount}
        text={"Click me to log with empty array as input"}
        color={"orange"}
      />
      <Block
        onBlockClick={updateCount}
        text={"Click me to add to the count"}
        color={"red"}
      />
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<Example />, rootElement);



 类似资料:
  • 问题内容: Parent(在我的示例中)组件通过Child()组件呈现一个数组。父级决定更改数组中的属性,React触发子级重新渲染的方式是什么? 调整数据后,我想出的只是在Parent中。这是触发更新的hack还是React方法? JS小提琴:https: //jsfiddle.net/69z2wepo/7601/ 问题答案: 这里的问题是您要在中存储状态,而不是。由于此组件是mutating

  • 考虑规范的示例: 单击按钮使每个状态打印两次。为什么呢?

  • 我对react还比较陌生,我一直在分解一个web应用程序,并用react组件替换部分。我现在正在开发一个组件,其中包含我创建的几个不同组件。 在新组件中,我在componentDidMount函数中进行API调用,并创建子组件。乍一看,一切看起来都很完美,但当我们在其中一个子组件中进行状态更改,然后在父组件中进行更改时,子组件将其状态重置为更改之前的状态。 我知道发生了什么,州政府没有被传递给家长

  • 嗨,我不确定这是一个期望的行为还是一个bug。 这是一个空的create react应用程序示例,带有 版本: 反应:^16.13.1, react-dom:^16.13.1, react-router-dom:^5.2.0, 反应脚本:3.4.1 部件: /-对于主部件 /触点-用于触点组件 这里提供小提琴 多次单击“主页”链接会显示一条

  • 问题内容: 我第一次尝试使用React钩子,直到我意识到当我获取数据并更新两个不同的状态变量(数据和加载标志)时,我的组件(数据表)都呈现了两次,即使两次调用都看起来不错状态更新程序发生在同一功能中。这是我的api函数,它将两个变量都返回给我的组件。 在普通的类组件中,您只需要调用一次即可更新状态,该状态可以是一个复杂的对象,但是“钩子方式”似乎是将状态分成较小的单元,其副作用似乎是多次分别更新时

  • 我对Minecraft 1 11中的纹理有问题,我创建了一个带有子项的Hammer类: 这是ItemInit类,我用纹理注册项目: 所有文件夹命名正确,全网搜索,找不到任何帮助 以下是一些屏幕截图: 所有三个*. json都有相同的文本: 你能提出什么解决办法吗?