React 的 Context API 重新渲染所有包含在 Context 中的组件

ans*_*hul 10 javascript reactjs

这是使用 Context API 时非常常见的性能问题。本质上,每当上下文中的状态值发生变化时,提供者之间包装的整个组件都会重新呈现并导致性能下降。

如果我有这样的包装:

<CounterProvider>
        <SayHello />
        <ShowResult />
        <IncrementCounter />
        <DecrementCounter />
</CounterProvider>
Run Code Online (Sandbox Code Playgroud)

价值道具为:

<CounterContext.Provider value={{increment, decrement, counter, hello }} >
  {children}
</CounterContext.Provider>
Run Code Online (Sandbox Code Playgroud)

每次我增加组件的计数值时IncrementCounter,整个包装组件集都会重新呈现,因为这就是 Context API 的工作方式。

我做了一些研究并发现了这些解决方案:

  1. 根据用例将上下文拆分为 N 个上下文:此解决方案按预期工作。
  2. 使用以下方式包装值提供者React.Memo:我看到很多文章建议使用 React.Memo API,如下所示:
<CounterContext.Provider
  value={useMemo(
    () => ({ increment, decrement, counter, hello }),
    [increment, decrement, counter, hello]
    )}
>
  {children}
</CounterContext.Provider>
Run Code Online (Sandbox Code Playgroud)

然而,这并没有按预期工作。我仍然可以看到所有组件都被重新渲染。使用 Memo API 时我做错了什么?Dan Abramov确实建议在一个开放的 React问题中采用这种方法

如果有人能帮我解决这个问题。谢谢阅读。

You*_*mar 11

“本质上,每当上下文中的状态值发生变化时,提供者之间包装的整个组件都会重新渲染并导致性能下降。”

如果像下面的示例中那样使用上下文,其中组件直接嵌套在提供程序中,则上述语句为 true。当变化时,它们都会重新渲染count,无论它们useContext(counterContext)是否调用过。

const counterContext = React.createContext();
const CounterContextProvider = () => {
  const [count, setCount] = React.useState(0);
  return (
    <counterContext.Provider value={{ count, setCount }}>
      <button onClick={() => setCount((prev) => prev + 1)}>Change state</button>
      <ComponentOne/>
      <ComponentTwo />
    </counterContext.Provider>
  );
};

const ComponentOne = () => {
  console.log("ComponentOne renders");
  return <div></div>;
};

const ComponentTwo = () => {
  console.log("ComponentTwo renders ");
  return <div></div>;
};
function App() {
  return (
    <CounterContextProvider/>
  );
}
ReactDOM.render(
  <App />,
  document.getElementById("root")
);
Run Code Online (Sandbox Code Playgroud)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Run Code Online (Sandbox Code Playgroud)

“本质上,每当上下文中的状态值发生变化时,提供者之间包装的整个组件都会重新渲染并导致性能下降。”

如果您使用childrenprop 来使用嵌套组件,则上述语句是错误的,如下例所示。

这次当count发生变化时,CounterContextProvider会渲染,但由于它的渲染是因为它的状态发生了变化,而不是因为它的父渲染,而且因为组件无法改变它的props,所以 React 不会渲染children

如果是普通组件的话就是这样。但由于这里涉及到上下文,React 将找到包含useContext(counterContext)并渲染它们的所有组件。

const counterContext = React.createContext();
const CounterContextProvider = ({ children }) => {
  const [count, setCount] = React.useState(0);
  return (
    <counterContext.Provider value={{ count, setCount }}>
      <button onClick={() => setCount((prev) => prev + 1)}>Change state</button>
      {children}
    </counterContext.Provider>
  );
};

const ComponentOne = () => {
  const { count } = React.useContext(counterContext);
  console.log("ComponentOne renders");
  return <div></div>;
};

const ComponentTwo = () => {
  console.log("ComponentTwo renders ");
  return <div></div>;
};
function App() {
  return (
    <CounterContextProvider>
      <ComponentOne />
      <ComponentTwo />
    </CounterContextProvider>
  );
}
ReactDOM.render(
  <App />,
  document.getElementById("root")
);
Run Code Online (Sandbox Code Playgroud)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Run Code Online (Sandbox Code Playgroud)

在上面的示例中,仅在更改ComponentOne时渲染count,这是正常的,因为他正在消耗它。如果上下文的一个值发生更改,则每个调用的组件useContext(counterContext)都会进行渲染。

即使像您那样useMemo包装上下文value对象,只要其依赖项数组中的一个变量发生更改,您就会得到这种行为。

如果这让您感到困扰,建议使用多个简单上下文,而不是使用value具有多个键值对的对象。