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

9

在使用Context API时,这是一个非常常见的性能问题。实际上,每当上下文中的状态值发生变化时,位于提供程序之间的整个组件都会重新渲染,从而导致性能下降。

如果我的包装器是这样的:

<CounterProvider>
        <SayHello />
        <ShowResult />
        <IncrementCounter />
        <DecrementCounter />
</CounterProvider>

并且价值主张如下:

<CounterContext.Provider value={{increment, decrement, counter, hello }} >
  {children}
</CounterContext.Provider>

每次我从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>

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

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

所有这些组件都使用上下文吗? - Konrad
是的,@KonradLinkowski 是一个例子。 - anshul
1个回答

10
“基本上,每当上下文中的状态值发生变化时,被提供程序包装的整个组件都会重新渲染,导致性能下降。”
上述语句是正确的,如果像下面的示例一样直接将组件嵌套在提供程序中使用上下文,则所有组件在计数更改时都会重新渲染,无论它们是否调用了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")
);
<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>

"基本上,每当上下文中的状态值发生更改时,包在提供程序之间的所有组件都会重新呈现并导致性能下降。"

如果您正在使用children属性消耗嵌套组件,如下例所示,则上述语句是错误的。

这次当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")
);
<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>

在上面的例子中,只有 ComponentOne count 更改时渲染,这是正常的原因,因为他正在消耗它。如果上下文更改了一个值,则调用 useContext(counterContext)的每个组件都会呈现。
即使像您所做的那样使用 useMemo 包装上下文 value 对象,只要其依赖项数组中的一个变量发生更改,就会出现这种行为。
如果这让您感到困扰,建议使用多个简单上下文,而不是具有多个键值对的 value 对象的上下文。

1
这是因为我假设您在DecrementCounter中有const { decrement } = useContext(CounterContext),在这种情况下,遗憾的是这就是行为。 - Youssouf Oumar
1
这就是为什么上下文被设计用于保存不经常更改的数据,例如已登录的用户或语言偏好... - Youssouf Oumar
1
是的,如果“上下文(context)”中的某个值发生了变化,每个使用“useContext(CounterContext)”的子组件都会重新渲染。这就是它的设计原则。即使使用了“useMemo”,一旦一个依赖变量发生变化,你也会得到这种行为。 - Youssouf Oumar
我相信如果不分割上下文,使用核心React无法改进这个问题,对吗? - anshul
1
@TusharShahi 我会尝试用一个例子来解释 - https://codesandbox.io/p/sandbox/white-pine-dzq1nn(在文档中找不到直接的解释)。这个想法对于任何接收children作为props的组件都是相同的。当你改变bg时,Comp不会被渲染,因为只有App可以向Comp传递信息(因为它创建Comp实例)。容器将Comp实例作为不可变对象的属性之一接收,因此当你改变bg(容器状态)时,没有必要重新渲染Comp,因为没有什么能够在那里改变。 - Alissa
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接