React Hooks useCallback 导致孩子重新渲染 [英] React Hooks useCallback causes child to re-render

查看:33
本文介绍了React Hooks useCallback 导致孩子重新渲染的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试使用新的 Hooks 从类组件转变为功能组件.但是感觉使用 useCallback 我会得到不必要的子元素渲染,这与类组件中的类函数不同.

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

类组件测试用例

class Block extends React.PureComponent {使成为() {console.log("渲染块:", this.props.color);返回 (<div onClick={this.props.onBlockClick}风格 = {{宽度:'200px',高度:'100px',marginTop: '12px',backgroundColor: this.props.color,文本对齐:'居中'}}>{this.props.text}

);}};类示例扩展 React.Component {状态 = {计数:0}onClick = () =>{console.log("当计数为:", this.state.count);}updateCount = () =>{this.setState({count: this.state.count + 1});};使成为() {console.log("渲染示例.计数:", this.state.count);返回 (<div style={{ display: 'flex', 'flexDirection': 'row'}}><Block onBlockClick={this.onClick} text={'点击我记录计数!'} color={'orange'}/><Block onBlockClick={this.updateCount} text={'点击我添加到计数'} color={'red'}/>

);}};ReactDOM.render(, 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%;高度:100%'>

功能组件测试用例

const Block = React.memo((props) => {console.log("渲染块:", props.color);返回 (<div onClick={props.onBlockClick}风格 = {{宽度:'200px',高度:'100px',marginTop: '12px',backgroundColor: props.color,文本对齐:'居中'}}>{道具.文本}

);});const 示例 = () =>{const [ count, setCount ] = React.useState(0);console.log("渲染示例.计数:", count);const onClickWithout = React.useCallback(() => {console.log("当计数为:", count);}, []);const onClickWith = React.useCallback(() => {console.log("当计数为:", count);}, [ 数数 ]);const updateCount = React.useCallback(() => {设置计数(计数 + 1);}, [ 数数 ]);返回 (<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={'点击我添加到计数'} color={'red'}/>

);};ReactDOM.render(, 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%;高度:100%'>

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

在第二个(功能组件)中,通过红色块更新计数将触发红色和青色块的重新渲染.这是因为 useCallback 将创建其函数的新实例,因为计数已更改,导致块获得新的 onClick 属性并因此重新渲染.橙色块不会重新渲染,因为用于橙色 onClickuseCallback 不依赖于计数值.这会很好,但是当您点击橙色块时,它不会显示计数的实际值.

我认为使用 useCallback 的目的是让孩子们不会获得相同功能的新实例,也不会进行不必要的重新渲染,但这似乎在第二次发生回调函数使用单个变量,根据我的经验,这种情况经常发生.

那么我如何在功能组件中实现这个 onClick 函数而不让孩子重新渲染?有可能吗?

更新(解决方案):使用下面 Ryan Cogswell 的回答,我精心制作了一个自定义钩子,以便轻松创建类函数.

const useMemoizedCallback = (callback, input = []) =>{//实例变量来保存实际的回调.const callbackRef = React.useRef(callback);//不会改变的记忆回调并调用改变的 callbackRef.const memoizedCallback = React.useCallback((...args) => {返回 callbackRef.current(...args);}, []);//根据输入不断更新的回调.const updatedCallback = React.useCallback(callback, input);//效果根据输入更新 callbackRef.React.useEffect(() => {callbackRef.current = updatedCallback;}, 输入);//返回记忆的回调.返回 memoizedCallback;};

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

const onClick = useMemoizedCallback(() => {console.log("NEW 我在计数时被点击:", count);}, [数数]);

解决方案

useCallback 将避免由于父项中部分的某些变化而导致不必要的子项重新渲染回调的依赖项.为了避免在涉及回调的依赖项时重新渲染子级,您需要使用 ref.Ref 是等效于实例变量的钩子.

下面我有 onClickMemoized 使用 onClickRef 指向当前的 onClick(通过 useEffect 设置)所以它委托给知道状态当前值的函数版本.

我还更改了 updateCount 以使用函数式更新语法,这样它就不需要依赖于 count.

const Block = React.memo(props => {console.log("渲染块:", props.color);返回 (

);});const 示例 = () =>{const [count, setCount] = React.useState(0);console.log("渲染示例.计数:", count);const onClick = () =>{console.log("当计数为:", count);};const onClickRef = React.useRef(onClick);React.useEffect(() =>{//通过省略依赖数组参数,这意味着//此效果将在每次提交渲染后执行,因此//onClickRef.current 将保持最新状态.onClickRef.current = onClick;});const onClickMemoized = React.useCallback(() => {onClickRef.current();}, []);const updateCount = React.useCallback(() => {setCount(prevCount => prevCount + 1);}, []);返回 (<div style={{ display: "flex", flexDirection: "row" }}><块onBlockClick={onClickMemoized}text="点击我以空数组作为输入登录"}颜色={橙色"}/><块onBlockClick={updateCount}text="点击我添加到计数"}颜色={红色"}/>

);};ReactDOM.render(, document.getElementById("root"));

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script><div id='root' style='width: 100%;高度:100%'>

当然,钩子的美妙之处在于您可以将这种有状态的逻辑分解为自定义钩子:

从反应"导入反应;从react-dom"导入 ReactDOM;const Block = React.memo(props => {console.log("渲染块:", props.color);返回 (

);});const useCount = () =>{const [count, setCount] = React.useState(0);const logCount = () =>{console.log("当计数为:",计数时我被点击了);};const logCountRef = React.useRef(logCount);React.useEffect(() => {//通过省略依赖数组参数,这意味着//此效果将在每次提交渲染后执行,因此//logCountRef.current 将保持最新.logCountRef.current = logCount;});const logCountMemoized = React.useCallback(() => {logCountRef.current();}, []);const updateCount = React.useCallback(() => {setCount(prevCount => prevCount + 1);}, []);return { count, logCount: logCountMemoized, updateCount };};const 示例 = () =>{const { count, logCount, updateCount } = useCount();console.log("渲染示例.计数:",计数);返回 (<div style={{ display: "flex", flexDirection: "row";}}><块onBlockClick={logCount}text={单击我以空数组作为输入进行记录"}颜色={橙色"}/><块onBlockClick={updateCount}text={点击我添加到计数"}颜色={红色"}/>

);};const rootElement = document.getElementById(root");ReactDOM.render(, rootElement);

I'm trying to turn from class components to functional components using the new Hooks. However it feels that with useCallback I will get unnecessary renders of children unlike with class functions in class components.

Below I have two relatively simple snippets. The first is my example written as classes, and the second is my example re-written as functional components. The goal is to get the same behaviour with functional components as with class components.

Class component test-case

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>

Functional component test-case

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>

In the first one(class components) I can update the count via the red block without re-rendering either of the blocks, and I can freely console log the current count via the orange block.

In the second one(functional components) updating the count via the red-block will trigger a re-render of both the red and cyan block. This is because the useCallback will make a new instance of it's function because the count has changed, causing the blocks to get a new onClick prop and thus re-render. The orange block won't re-render because the useCallback used for the orange onClick does not depend on the count value. This would be good but the orange block will not show the actual value of the count when you click on it.

I thought the point of having useCallback was so that children don't get new instances of the same function and don't have unnecessary re-renders, but that seems to happen anyways the second the callback function uses a single variable which happens quite often if not always from my experience.

So how would I go about making this onClick function within a functional component without having the children re-render? Is it at all possible?

Update (solution): Using Ryan Cogswell's answer below I've crafted a custom hook to make creating class-like functions easely.

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;
};

I can then use this in the function component very easely like so and simply pass the onClick to the child. It will no longer re-render the child but still make use of updated vars.

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

解决方案

useCallback will avoid unnecessary child re-renders due to something changing in the parent that is not part of the dependencies for the callback. In order to avoid child re-renders when the callback's dependencies are involved, you need to use a ref. Ref's are the hook equivalent to an instance variable.

Below I have onClickMemoized using the onClickRef which points at the current onClick (set via useEffect) so that it delegates to a version of the function that knows the current value of the state.

I also changed updateCount to use the functional update syntax so that it doesn't need to have a dependency on 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(
    () => {
      // By leaving off the dependency array parameter, it means that
      // this effect will execute after every committed render, so
      // onClickRef.current will stay up-to-date.
      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.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>

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

And, of course, the beauty of hooks is that you can factor out this stateful logic into a custom hook:

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(() => {
    // By leaving off the dependency array parameter, it means that
    // this effect will execute after every committed render, so
    // logCountRef.current will stay up-to-date.
    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);

这篇关于React Hooks useCallback 导致孩子重新渲染的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
其他开发最新文章
热门教程
热门工具
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆