React 上下文、性能?

2025-06-10

React 上下文、性能?

今天我们来聊聊React context。它的作用有时会被误解,被错误地称为 mini-redux。首先,我们来了解一下它是什么,然后再讨论它的性能以及一些可用的解决方法。

它是什么?

我无法找到比文档中更好的定义:

Context 提供了一种通过组件树传递数据的方法,而无需在每个级别手动传递 props。

从概念上讲,你将数据放入React 上下文中,并通过Provider将其提供给 React 子树组件。然后,在这个子树中的所有组件中,你都可以通过Consumer获取数据。上下文中数据的每次变化都会通知每个消费者。

所以这里没有状态管理的概念,所以不要混淆,React context 不是一个迷你版的 redux。但是如果你将它与state或结合使用,你可以模拟它reducer。然而,你必须知道redux提供了一些功能,例如:

  • 时间旅行
  • 中间件
  • 性能优化

如何使用 React 上下文

创建上下文

createContext创建过程通过pulls from 方法完成React。该方法仅接受可选的默认值作为参数:

const MyContext = React.createContext();
Enter fullscreen mode Exit fullscreen mode

提供者

可以Provider通过创建的上下文进行访问:

const MyProvider = MyContext.Provider;
Enter fullscreen mode Exit fullscreen mode

Provider获得的组件具有以下属性:

  • 值:您想要向子组件提供
  • children:您想要提供价值的子项
<MyProvider value={valueToProvide}>
  {children}
</MyProvider>
Enter fullscreen mode Exit fullscreen mode

注意:创建一个Provider接受组件children而不是直接将组件放入提供程序中,如下所示:

function App() {
  const [data, setData] = useState(null);

  return (
    <MyContext.Provider value={{ data, setData }}>
      <Panel>
        <Title />
        <Content />
      </Panel>
    </MyContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

每次调用时都这样做setData,它将渲染所有组件TitleContent即使Panel它们不使用data

所以应该这样做:

function MyProvider({ children }) {
  const [data, setData] = useState(null);

  return (
    <MyContext.Provider value={{ data, setData }}>
      {children}
    </MyContext.Provider>
  );
}

function App() {
  return (
    <MyProvider>
      <Panel>
        <Title />
        <Content />
      </Panel>
    </MyProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

消费者

一旦我们提供了一些数据,我们可能希望在子级中获取它。有两种方法可以获取它:

  • 带钩useContext
  • 使用Consumer我们创建的上下文提供的组件

useContext

这是一个从上下文中获取值的钩子。你只需要将上下文传递给钩子即可:

const myValue = useContext(MyContext);
Enter fullscreen mode Exit fullscreen mode

注意:您可能希望创建一个自定义钩子,useMyContext而不是直接导出上下文。这将允许您进行一些检查,以确保Provider已正确添加:

const useMyContext = () => {
  const value = useContext(MyContext);

  if (!value) {
    throw new Error(
      "You have to add the Provider to make it work"
    );
  }

  return value;
};
Enter fullscreen mode Exit fullscreen mode

警告:如果在上下文中输入默认值,则检查将不起作用。

Consumer成分

如前所述,创建的上下文也会导出一个Consumer组件(如Provider),然后您可以通过将函数作为子函数传递来获取值:

<MyContext.Consumer>
  {(value) => {
    // Render stuff
  }
</MyContext.Consumer>
Enter fullscreen mode Exit fullscreen mode

推荐和财产

将上下文放在最接近其使用的地方

建议将Providers 放在最靠近使用位置的地方。我的意思是,不要把所有Providers 都放在应用的顶部。这样可以帮助你深入代码库,实现关注点分离,并且应该会略微提高 React 的速度,因为这样就不必遍历所有组件树了。

注意:您需要将一些内容放在申请Provider的顶部。例如:I18nProvider,,,...SettingsProviderUserProvider

这样做时,如果您将对象作为值传递(大多数情况下都是这种情况),则在父级重新渲染时可能会遇到一些性能问题。

例如,如果您有:

const MyContext = React.createContext();

function MyProvider({ children }) {
  const [data, setData] = useState(null);

  const onClick = (e) => {
    // Whatever process
  };

  return (
    <MyContext.Provider value={{ data, onClick }}>
      {children}
    </MyContext.Provider>
  );
}

function ComponentUsingContext() {
  const { onClick } = useContext(MyContext);

  return <button onClick={onClick}>Click me</button>;
}

const MemoizedComponent = React.memo(ComponentUsingContext);

function App() {
  const [counter, setCount] = useState(0);

  return (
    <div>
      <button
        onClick={() => setCounter((prev) => prev + 1)}
      >
        Increment counter: counter
      </button>
      <MyProvider>
        <MemoizedComponent />
      </MyProvider>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

在这种情况下,当我们增加计数器时,MemoizedComponent即使它被记忆,也会重新渲染,因为上下文中的值发生了变化。

在这种情况下,解决方案是记住该值:

const value = useMemo(() => {
  const onClick = (e) => {
    // Whatever process
  };

  return {
    data,
    onClick,
  };
}, [data]);
Enter fullscreen mode Exit fullscreen mode

好了,MemoizedComponent增加计数器时不再渲染。

警告:仅当使用上下文的组件被记忆时,记忆值才有用。否则,它将什么也不做,并继续重新渲染,因为父组件会重新渲染(在上一种情况下,父组件是 App)。

嵌套提供程序

可以在相同的上下文中使用嵌套的 Provider。例如,在react-router实现中使用,请参阅我的文章

在这种情况下,消费者将获得距离他们最近的提供者的值。

const MyContext = React.createContext();

export default function App() {
  return (
    <MyContext.Provider value="parent">
      <ParentSubscriber />
      <MyContext.Provider value="nested">
        <NestedSubscriber />
      </MyContext.Provider>
    </MyContext.Provider>
  );
}

function ParentSubscriber() {
  const value = useContext(MyContext);

  return <p>The value in ParentSubscriber is: {value}</p>;
}

function NestedSubscriber() {
  const value = useContext(MyContext);

  return <p>The value in NestedSubscriber is: {value}</p>;
}
Enter fullscreen mode Exit fullscreen mode

在前面的例子中,ParentSubscriber将获得值parent,而在另一端NestedSubscriber将获得nested


表现

为了讨论性能,我们将制作一个具有一些功能的小型音乐应用程序:

  • 能够看到我们的朋友正在听什么
  • 表演音乐
  • 显示当前音乐

重要提示:请勿评判 React context 功能的使用。我知道,在实际应用中,我们可能不会使用此类功能。

朋友和音乐功能

规格:

  • 好友功能包括每 2 秒获取一次虚假 API,该 API 将返回以下类型的对象数组:
type Friend = {
  username: string;
  currentMusic: string;
}
Enter fullscreen mode Exit fullscreen mode
  • 音乐功能将仅获取一次可用的音乐并返回:
type Music = {
  uuid: string; // A unique id
  artist: string;
  songName: string;
  year: number;
}
Enter fullscreen mode Exit fullscreen mode

好的。让我们来实现它。
我只是想把所有这些数据放在同一个context中,然后提供给我的应用程序。

让我们实现 Context 和 Provider:

import React, {
  useContext,
  useEffect,
  useState,
} from "react";

const AppContext = React.createContext();

// Simulate a call to a musics API with 300ms "lag"
function fetchMusics() {
  return new Promise((resolve) =>
    setTimeout(
      () =>
        resolve([
          {
            uuid: "13dbdc18-1599-4a4d-b802-5128460a4aab",
            artist: "Justin Timberlake",
            songName: "Cry me a river",
            year: 2002,
          },
        ]),
      300
    )
  );
}

// Simulate a call to a friends API with 300ms "lag"
function fetchFriends() {
  return new Promise((resolve) =>
    setTimeout(() => {
      resolve([
        {
          username: "Rainbow",
          currentMusic:
            "Justin Timberlake - Cry me a river",
        },
      ]);
    }, 300)
  );
}

export const useAppContext = () => useContext(AppContext);

export default function AppProvider({ children }) {
  const [friends, setFriends] = useState([]);
  const [musics, setMusics] = useState([]);

  useEffect(() => {
    fetchMusics().then(setMusics);
  }, []);

  useEffect(() => {
    // Let's poll friends every 2sec
    const intervalId = setInterval(
      () => fetchFriends().then(setFriends),
      2000
    );

    return () => clearInterval(intervalId);
  }, []);

  return (
    <AppContext.Provider value={{ friends, musics }}>
      {children}
    </AppContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Friends现在我们来看看and组件的实现Musics。没什么复杂的:

function Friends() {
  const { friends } = useAppContext();

  console.log("Render Friends");

  return (
    <div>
      <h1>Friends</h1>
      <ul>
        {friends.map(({ username, currentMusic }) => (
          <li key={username}>
            {username} listening {currentMusic}
          </li>
        ))}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

和:

function Musics() {
  const { musics } = useAppContext();

  console.log("Render Musics");

  return (
    <div>
      <h1>My musics</h1>
      <ul>
        {musics.map(({ uuid, artist, songName, year }) => (
          <li key={uuid}>
            {artist} - {songName} ({year})
          </li>
        ))}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

现在,我问你一个问题。你知道控制台里会渲染/打印什么吗?

是的, 和Friends都会Musics大约每 2 秒渲染一次。为什么?
你还记得我告诉过你,如果提供的值发生变化,每个消费者都会被触发,即使他们使用了该值中不变的部分。
这种情况只会Musicsmusics上下文中提取不变的 。

您可以在以下codesandbox中看到它:

这就是为什么我建议不同的环境中按业务领域分离数据

在我们的例子中,我将创建两个单独的上下文FriendsContextMusicContext

您可以在此处看到实现:


当前正在听的音乐

现在我们希望能够从列表中选择一首音乐并收听。

我将创建一个新的上下文来存储currentMusic

import React, { useContext, useState } from "react";

const CurrentMusicContext = React.createContext();

export const useCurrentMusicContext = () =>
  useContext(CurrentMusicContext);

export default function CurrentMusicProvider({ children }) {
  const [currentMusic, setCurrentMusic] =
    useState(undefined);

  return (
    <CurrentMusicContext.Provider
      value={{ currentMusic, setCurrentMusic }}
    >
      {children}
    </CurrentMusicContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

我在组件中添加了一个按钮Musics来收听相关音乐:

function MyMusics() {
  const musics = useMusicContext();
  const { setCurrentMusic } = useCurrentMusicContext();

  console.log("Render MyMusics");

  return (
    <div>
      <h1>My musics</h1>
      <ul>
        {musics.map((music) => (
          <li key={music.uuid}>
            {getFormattedSong(music)}{" "}
            <button onClick={() => setCurrentMusic(music)}>
              Listen
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

CurrentMusic组件非常简单:

function CurrentMusic() {
  const { currentMusic } = useMusicContext();

  console.log("Render CurrentMusic");

  return (
    <div>
      <h1>Currently listening</h1>
      {currentMusic ? (
        <strong>{getFormattedSong(currentMusic)}</strong>
      ) : (
        "You're not listening a music"
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

好的,当您选择听一首新音乐时会发生什么?

目前,和MyMusics都会CurrentMusic渲染。因为当currentMusic发生变化时,会有一个新对象传递给提供程序。

分离dynamicstatic数据

一种策略是将动态数据和静态数据分离在两个不同的上下文CurrentMusicDynamicContextCurrentMusicStaticContext

import React, { useContext, useState } from "react";

const CurrentMusicStaticContext = React.createContext();
const CurrentMusicDynamicContext = React.createContext();

export const useCurrentMusicStaticContext = () =>
  useContext(CurrentMusicStaticContext);
export const useCurrentMusicDynamicContext = () =>
  useContext(CurrentMusicDynamicContext);

export default function CurrentMusicProvider({ children }) {
  const [currentMusic, setCurrentMusic] =
    useState(undefined);

  return (
    <CurrentMusicDynamicContext.Provider
      value={currentMusic}
    >
      <CurrentMusicStaticContext.Provider
        value={setCurrentMusic}
      >
        {children}
      </CurrentMusicStaticContext.Provider>
    </CurrentMusicDynamicContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

现在我们开始吧。只需使用正确的钩子从上下文中获取值。

注意:正如您所看到的,它确实使代码变得复杂,这就是为什么不要尝试修复不存在的性能问题。


use-context-selector

第二种解决方案是使用dai-shi开发的库use-context-selector。我写了一篇关于其实现的文章。
它将包装 React 的原生上下文 API,让你可以访问多个钩子,只有当 store 中选定的值发生变化时,这些钩子才会重新渲染你的组件。

createContext原理很简单,你可以通过库提供的函数创建上下文。
然后使用 从中获取数据useContextSelector。API 如下:

useContextSelector(CreatedContext, valueSelectorFunction)
Enter fullscreen mode Exit fullscreen mode

例如如果我想获得currentMusic

const currentMusic = useContextSelector(
  CurrentMusicContext,
  (v) => v.currentMusic
);
Enter fullscreen mode Exit fullscreen mode

为了不暴露上下文,我做了一个钩子:

export const useCurrentMusicContext = (selector) =>
  useContextSelector(CurrentMusicContext, selector);
Enter fullscreen mode Exit fullscreen mode

就这样。代码如下:


结论

我们已经了解了如何使用 React 上下文,以及你可能遇到的性能问题。
但一如既往,不要过早进行优化。等到真正出现问题时再考虑。
正如你所见,优化会使你的代码可读性下降,代码更加冗长。
只需尝试将不同的业务逻辑分离到不同的上下文中,并将提供程序尽可能靠近需要的地方,以使代码更清晰。不要将所有内容都放在应用程序的顶部。
如果由于上下文而遇到真正的性能问题,你可以:

  • 在不同的上下文中分离动态和静态数据
  • useMemo如果父组件重新渲染导致值发生变化,则该值会被重置。但你必须memo在使用上下文(或父组件)的组件上放置一些值,否则它不会执行任何操作。
  • 使用这个use-context-selector库来解决 context 的缺陷。也许有一天会原生地实现,正如你在这个已打开的 PRreact中看到的那样
  • 我们在本文中没有讨论的另一种策略是不要使用 React 上下文,而是使用原子状态管理库,例如:jotai,,recoil...

请随时发表评论,如果您想了解更多信息,您可以在Twitter上关注我或访问我的网站

鏂囩珷鏉ユ簮锛�https://dev.to/romaintrotard/react-context-performance-5832
PREV
使用 Laravel Sanctum 构建基于令牌的 Vue.js 客户端 SPA 身份验证
NEXT
为什么要将 REST API 文档化为代码?TL;DR 主题分解 为什么要将 REST API 文档化为代码?什么是 OpenAPI?OpenAPI 的适用场景是什么?工具是什么?下一步是什么?延伸阅读