React Hooks useCallback 导致孩子重新渲染

IT技术 reactjs react-hooks
2021-03-31 12:33:19

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

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

类组件测试用例

功能组件测试用例

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

在第二个(功能组件)中,通过红色块更新计数将触发红色和青色块的重新渲染。这是因为useCallback将创建其函数的新实例,因为计数已更改,导致块获得新的onClickprops并因此重新渲染。橙色块不会重新渲染,因为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 传递给孩子。它将不再重新渲染孩子,但仍会使用更新的变量。

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

useCallback将避免不必要的子项重新渲染,因为父项中的某些更改不是回调依赖项的一部分。为了避免在涉及回调的依赖项时重新渲染子级,您需要使用 ref。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(
    () => {
      // 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>

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

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

编辑 useCallback 和 useRef

谢谢你的回答。我确实发现在具有单一函数的类组件中可以执行的操作会在此功能组件中采用实际函数,1 个引用将函数存储在实例 var 中,1 个记忆化函数传递给子级,并且1useEffect更新实例变量。在我接受这个作为答案之前,我想知道您是否认为可以将这 4 个放在自定义钩子中,以便它仍然是一个简单的单一函数调用?
2021-05-25 12:33:19
@mowwwalker 我在很多地方都读到过它,但我没有很好的参考资料可供您参考。有关准备 StrictMode 和并发渲染的文章将涉及它。我在 Twitter 上关注了几个 React 团队,并在那里看到了一些相关的对话(例如在 Twitter 上搜索“@sebmarkbage ref”)。
2021-05-29 12:33:19
我忘记保存最新版本的沙箱——链接已更新。
2021-06-06 12:33:19
是的,这就是钩子的魅力所在。答案已更新。
2021-06-17 12:33:19
@mowwwalker 因为渲染不应该有任何副作用(例如设置引用),因为渲染可能不会被提交;而只有在渲染已提交给 DOM 时才会执行效果。
2021-06-19 12:33:19

这也适用于当前代码的最小更改。

  • useCallback 中的 deps 参数被删除
  • 当状态改变时更新 ref 值
  • 不要使用来自状态的值,而是使用来自useCallback内的 ref 的值,因为 deps 被删除并且状态值不会被更新。

const {useState} = React

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 ] = useState(0);
  
  const countRef = React.useRef(count);
  console.log("Rendering Example. Count: ", count);
  
  const onClickWithout = React.useCallback(() => {
    console.log("I've been clicked when count was: ", count, countRef.current);
  }, []);
  
  const onClickWith = React.useCallback(() => {
    console.log("I've been clicked when count was: ", count, countRef.current);
  }, [ ]);
  
  const updateCount = React.useCallback(() => {
    setCount(count => { 
      countRef.current = count+1
      return count + 1 
    });
  }, [ ]);
  
  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("react"));
//ReactDOM.render( < Example / > , document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>