首页 > 解决方案 > 使用 useMemo() 防止上下文重新渲染

问题描述

在研究上下文时,我不止一次偶然发现了一个常见模式:

const storeContext = React.createContext()

const Provider = (children) => {
  const [state, setState] = React.useState();

  const ctxValue = React.useMemo(() => [state, setState], [state]);

  return (
    <storeContext.Provider value={ctxValue}>
      {children}
    </storeContext.Provider>
  )
}

很多人似乎useMemo在这种情况下使用,我不知道为什么。乍一看,这似乎是有道理的,因为直接传递一个对象/数组意味着这个对象将在每次渲染时重新创建,并在传递一个新引用后触发子级重新渲染。乍一看,我对这种模式感到困惑。

我不确定这种模式如何可能阻止任何重新渲染。如果树上的一个组件发生更改,所有子项(包括上下文)无论如何都会重新呈现,如果state发生更改,则提供者(正确地)重新呈现自己以及所有子项。

我看到的唯一一件事useMemo就是节省一点内存,因为我们不是ctxValue在每次渲染时都重新创建对象。

我不确定我是否在这里遗漏了一些东西,并且会喜欢一些输入。

标签: reactjs

解决方案


原因是每次渲染都会创建一个新的 Array 实例。通过使用 useMemo,传递给上下文的数组仅在状态更改时才会更改。更好的方法是将 useState 的结果传递给上下文:

const storeContext = React.createContext()

const Provider = (children) => {
  const ctxValue = React.useState();

  return (
    <storeContext.Provider value={ctxValue}>
      {children}
    </storeContext.Provider>
  )
}

更新

所以,我很好奇 React 将如何处理重新渲染和上下文更新。我真的很惊讶地发现 React 只会在 props 发生变化时重新渲染组件。

例如,如果你有这样的事情:

function MyComponent(){
  const ctxValue = useContext(MyContext);
  return (<div>
    value: {ctxValue}
    <MyOtherComponent />
  </div>);
}

你渲染那个组件,MyOtherComponent永远只会渲染一次,假设组件不使用任何可以更新其内部状态的钩子。

Context.Provider 也是如此。它会重新渲染的唯一时间是值更新时,但其子级不会重新渲染,除非它们直接依赖于该上下文。

我创建了一个简单的沙箱来演示这一点。

基本上,我设置了很多不同的方式来嵌套孩子,有些孩子依赖上下文,有些则不。如果单击“+”按钮,上下文将更新,您将看到渲染计数仅增加这些组件。

const Ctx = React.createContext();

function useRenderCount(){
  const count = React.useRef(0);

  count.current += 1;

  return count.current;
}

function wrapInRenderCount(name,child){
  const count = useRenderCount();
  return (
    <div>
      <div>
        {name} was rendered {count} times.
      </div>
      <div style={{ marginLeft: "1em" }}>{child}</div>
    </div>
  );

}

function ContextProvider(props) {
  const [count, setState] = React.useState(0);

  const increase = React.useCallback(() => {
      setState((v) => v + 1);
  }, []);

  const context = React.useMemo(() => [increase, count], [increase, count]);

  return wrapInRenderCount('ContextProvider',
    <Ctx.Provider value={context}>
      {props.children}
    </Ctx.Provider>
  );
}

function ContextUser(props){
  const [increase, count] = React.useContext(Ctx);

  return wrapInRenderCount('ContextUser',
    <div>
      <div>
        Count: {count}
        <button onClick={increase}>
          ++
        </button>
      </div>
      <div>
        {props.children}
      </div>
    </div>
  );
}

function RandomWrapper(props){
  return wrapInRenderCount('RandomWrapper',<div>
    <div>
      Wrapped
    </div>
    <div style={{marginLeft:'1em'}}>
      {props.children}
    </div>
  </div>);
}

function RandomContextUserWrapper(props){
  return wrapInRenderCount('RandomContextUserWrapper',
    <div>
      <ContextUser></ContextUser>
      {props.children}
    </div>
  )
}

function App() {
  return wrapInRenderCount('App',
    <RandomWrapper>
      <ContextProvider>
          <RandomWrapper>
            <ContextUser>
              <RandomWrapper>
                <RandomContextUserWrapper>
                  <RandomWrapper>
                    <ContextUser/>
                    </RandomWrapper>
                </RandomContextUserWrapper>
              </RandomWrapper>
            </ContextUser>
            <RandomWrapper>
                <RandomContextUserWrapper />
              </RandomWrapper>
          </RandomWrapper>
      </ContextProvider>
    </RandomWrapper>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(
  <App />,
  rootElement
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>

<div id="root"></div>


推荐阅读