发布于 2026-01-06 0 阅读
0

过度使用 React Context 的弊端

过度使用 React Context 的弊端

作者:易卜拉希马·恩多✏️

大多数情况下,React 和状态管理密不可分。随着 React 应用的增长,状态管理变得越来越重要。

随着React 16.8 和 Hooks 的引入,React Context API 得到了显著改进。现在我们可以将其与 Hooks 结合使用,实现类似 `<script>` 标签的功能react-redux;有些人甚至用它来管理整个应用程序的状态。然而,React Context 也存在一些缺陷,过度使用可能会导致性能问题。

在本教程中,我们将回顾过度使用 React Context 的潜在后果,并讨论如何在下一个 React 项目中有效地使用它。

什么是 React Context?

React Context 提供了一种在应用程序中共享数据(状态)的方法,无需在每个组件中传递 props。它使你能够通过提供者和消费者来使用 Context 中保存的数据,而无需进行 props 传递。

const CounterContext = React.createContext();

const CounterProvider = ({ children }) => {
  const [count, setCount] = React.useState(0);

  const increment = () => setCount(counter => counter + 1);
  const decrement = () => setCount(counter => counter - 1);
  return (
    <CounterContext.Provider value={{ count, increment, decrement }}>
      {children}
    </CounterContext.Provider>
  );
};

const IncrementCounter = () => {
  const { increment } = React.useContext(CounterContext);
  return <button onClick={increment}> Increment</button>;
};

const DecrementCounter = () => {
  const { decrement } = React.useContext(CounterContext);
  return <button onClick={decrement}> Decrement</button>;
};

const ShowResult = () => {
  const { count } = React.useContext(CounterContext);
  return <h1>{count}</h1>;
};

const App = () => (
  <CounterProvider>
    <ShowResult />
    <IncrementCounter />
    <DecrementCounter />
  </CounterProvider>
);
Enter fullscreen mode Exit fullscreen mode

请注意,我特意将IncrementCounter其拆分DecrementCounter为两个部分。这有助于我更清晰地演示与 React Context 相关的问题。

如您所见,我们的上下文非常简单。它包含两个函数,increment分别decrement用于计算计数器及其结果。然后,我们从每个组件中获取数据并将其显示在App组件上。没什么特别的,就是一个典型的 React 应用。

戈登·拉姆齐问道:“问题出在哪里?”

从这个角度来看,你可能会想,使用 React Context 有什么问题呢?对于这样一个简单的应用来说,管理状态很容易。然而,随着应用变得越来越复杂,React Context 很快就会成为开发者的噩梦。

LogRocket 免费试用横幅

使用 React Context 的优缺点

虽然 React Context 实现起来很简单,而且非常适合某些类型的应用程序,但它的构建方式使得每次 context 的值发生变化时,组件使用者都会重新渲染。

目前为止,这对我们的应用来说还不是问题,因为如果组件在上下文值改变时不重新渲染,它就永远无法获取更新后的值。但是,重新渲染的范围将不仅限于组件的使用者;所有与该上下文相关的组件都会重新渲染。

为了更直观地了解它的作用,让我们更新一下例子。

const CounterContext = React.createContext();

const CounterProvider = ({ children }) => {
  const [count, setCount] = React.useState(0);
  const [hello, setHello] = React.useState("Hello world");

  const increment = () => setCount(counter => counter + 1);
  const decrement = () => setCount(counter => counter - 1);

  const value = {
    count,
    increment,
    decrement,
    hello
  };

  return (
    <CounterContext.Provider value={value}>{children}</CounterContext.Provider>
  );
};

const SayHello = () => {
  const { hello } = React.useContext(CounterContext);
  console.log("[SayHello] is running");
  return <h1>{hello}</h1>;
};

const IncrementCounter = () => {
  const { increment } = React.useContext(CounterContext);
  console.log("[IncrementCounter] is running");
  return <button onClick={increment}> Increment</button>;
};

const DecrementCounter = () => {
  console.log("[DecrementCounter] is running");
  const { decrement } = React.useContext(CounterContext);
  return <button onClick={decrement}> Decrement</button>;
};

const ShowResult = () => {
  console.log("[ShowResult] is running");
  const { count } = React.useContext(CounterContext);
  return <h1>{count}</h1>;
};

const App = () => (
  <CounterProvider>
    <SayHello />
    <ShowResult />
    <IncrementCounter />
    <DecrementCounter />
  </CounterProvider>
);
Enter fullscreen mode Exit fullscreen mode

我添加了一个新组件,SayHello用于显示来自上下文的消息。每当这些组件渲染或重新渲染时,我们也会记录一条消息。这样,我们就可以看到更改是否影响了所有组件。

// Result of the console
 [SayHello] is running
 [ShowResult] is running
 [IncrementCounter] is running
 [DecrementCounter] is running
Enter fullscreen mode Exit fullscreen mode

页面加载完成后,所有消息都会显示在控制台中。目前为止一切正常,无需担心。

我们点击increment按钮看看会发生什么。

// Result of the console
 [SayHello] is running
 [ShowResult] is running
 [IncrementCounter] is running
 [DecrementCounter] is running
Enter fullscreen mode Exit fullscreen mode

如您所见,所有组件都会重新渲染。点击decrement按钮也会产生同样的效果。每次上下文值发生变化时,所有组件的使用者都会重新渲染。

你可能还在想,这有什么关系呢?React Context 不就是这样工作的吗?

《老友记》里的乔伊耸了耸肩

对于这样的小型应用,我们不必担心使用 React Context 的负面影响。但在状态频繁变更的大型项目中,这个工具带来的问题远比它解决的问题多。一个简单的状态变更就可能导致无数次的重新渲染,最终造成严重的性能问题。

那么,我们如何避免这种会降低性能的重新渲染呢?

防止重新渲染useMemo()

或许记忆化是解决问题的办法。让我们更新一下代码,useMemo看看记忆化值能否帮助我们避免重新渲染。

const CounterContext = React.createContext();

const CounterProvider = ({ children }) => {
  const [count, setCount] = React.useState(0);
  const [hello, sayHello] = React.useState("Hello world");

  const increment = () => setCount(counter => counter + 1);
  const decrement = () => setCount(counter => counter - 1);

  const value = React.useMemo(
    () => ({
      count,
      increment,
      decrement,
      hello
    }),
    [count, hello]
  );

  return (
    <CounterContext.Provider value={value}>{children}</CounterContext.Provider>
  );
};

const SayHello = () => {
  const { hello } = React.useContext(CounterContext);
  console.log("[SayHello] is running");
  return <h1>{hello}</h1>;
};

const IncrementCounter = () => {
  const { increment } = React.useContext(CounterContext);
  console.log("[IncrementCounter] is running");
  return <button onClick={increment}> Increment</button>;
};

const DecrementCounter = () => {
  console.log("[DecrementCounter] is running");
  const { decrement } = React.useContext(CounterContext);
  return <button onClick={decrement}> Decrement</button>;
};

const ShowResult = () => {
  console.log("[ShowResult] is running");
  const { count } = React.useContext(CounterContext);
  return <h1>{count}</h1>;
};

const App = () => (
  <CounterProvider>
    <SayHello />
    <ShowResult />
    <IncrementCounter />
    <DecrementCounter />
  </CounterProvider>
);
Enter fullscreen mode Exit fullscreen mode

现在我们increment再点击一下按钮,看看是否有效。

<// Result of the console
 [SayHello] is running
 [ShowResult] is running
 [IncrementCounter] is running
 [DecrementCounter] is running
Enter fullscreen mode Exit fullscreen mode

遗憾的是,我们仍然遇到同样的问题。每当上下文值发生变化时,所有组件的使用者都会重新渲染。

迈克尔·斯科特做了个悲伤的表情

如果记忆无法解决问题,我们是否应该完全停止使用 React Context 管理状态?

是否应该使用 React Context?

在开始项目之前,你应该先确定如何管理应用状态。有很多解决方案可供选择,React Context 只是其中之一。为了确定哪种工具最适合你的应用,请问自己两个问题:

  1. 何时应该使用它?
  2. 你打算如何使用它?

如果你的状态需要频繁更新,React Context 可能不如React Redux等工具高效。但如果你拥有的是更新频率较低的静态数据,例如首选语言、时间变更、位置变更和用户身份验证,那么使用 React Context 传递 props 可能是最佳选择。

如果你选择使用 React Context,请尽可能将大型 Context 拆分成多个 Context,并将状态保持在组件使用者附近。这将有助于你最大限度地发挥 React Context 的特性和功能,在某些情况下,对于简单的应用程序来说,这会非常强大。

那么,是否应该使用 React Context 呢?答案取决于何时以及如何使用。

最后想说的

React Context 对于状态变更不频繁的简单应用来说是一个优秀的 API,但如果将其过度用于更复杂的项目,它很快就会变成开发者的噩梦。了解该工具在构建高性能应用时的工作原理,有助于判断它是否适合用于管理项目中的状态。尽管 React Context 在处理高频状态变更时存在局限性,但如果使用得当,它仍然是一个非常强大的状态管理解决方案。


全面了解生产环境中的 React 应用

调试 React 应用可能很困难,尤其是在用户遇到难以重现的问题时。如果您有兴趣监控和跟踪 Redux 状态、自动发现 JavaScript 错误以及跟踪缓慢的网络请求和组件加载时间,不妨试试 LogRocket。

替代文字

LogRocket就像 Web 应用的 DVR,它会记录 React 应用中发生的一切。你无需猜测问题原因,即可汇总并报告问题发生时应用的状态。LogRocket 还会监控应用的性能,并提供客户端 CPU 负载、客户端内存使用情况等指标的报告。

LogRocket Redux 中间件包为用户会话增加了一层额外的可见性。LogRocket 会记录 Redux 存储中的所有操作和状态。

革新 React 应用的调试方式——立即开始免费监控。


这篇文章《过度使用 React Context 的陷阱》最初发表在LogRocket 博客上。

文章来源:https://dev.to/bnevilleoneill/pitfalls-of-overusing-react-context-if5