4、React中的useContext:跨组件状态共享、Provider与Consumer、Context性能优化

讲完 useReducer,咱们来聊聊另一个日常高频使用的 Hook —— useContext。

说实话,我刚接触 React 那会儿,对 Context 的理解特别浅。就觉得它是个「全局变量」,哪都能用。结果项目一复杂,性能问题全冒出来了。后来踩了不少坑,才慢慢摸清楚它的脾气。

今天这一章,我就把 Context 的来龙去脉、最佳实践、还有那些容易翻车的地方,一次性给你讲透。

为什么需要 Context?

先想一个问题:如果有个「用户登录信息」,需要在页面最顶层的导航栏、中间的侧边栏、底部的个人中心都用上,你怎么传?

用 props 一层层往下传?那叫「prop drilling」。组件树深一点,中间那些不关心这个数据的组件,也得帮着传一遍。代码又臭又长,改起来还容易漏。

Context 就是来解决这个问题的。它让你能把数据「注入」到组件树的某个层级,然后下面所有子组件都能直接读取,不用经过中间层。

核心思想: Context 提供了一种「订阅-发布」机制。数据变化时,所有订阅了这个 Context 的组件都会自动重新渲染。

Provider 与 Consumer:两代写法

Context 的使用分两派:老派的 Consumer 模式,和新派的 useContext Hook。我建议你直接学新的,但老代码里经常见到 Consumer,所以也得认识一下。

1. 创建 Context

const ThemeContext = React.createContext('light');

createContext 接受一个默认值。这个默认值只在「没有 Provider 包裹」时生效。

2. Provider:数据的源头

function App() {
  const [theme, setTheme] = useState('dark');

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

Provider 的 value 变了,所有消费这个 Context 的子组件都会重新渲染。嗯,这里就是性能问题的根源,后面细说。

3. Consumer:老式写法

function Toolbar() {
  return (
    <ThemeContext.Consumer>
      {({ theme, setTheme }) => (
        <button onClick={() => setTheme('light')}>
          当前主题:{theme}
        </button>
      )}
    </ThemeContext.Consumer>
  );
}

Consumer 用了一个「render props」模式。说实话,嵌套多了可读性很差。我个人很少用这种写法。

4. useContext:现代写法

function Toolbar() {
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <button onClick={() => setTheme('light')}>
      当前主题:{theme}
    </button>
  );
}

简洁多了,对吧?useContext 直接返回 Context 的当前值,想用哪个字段就用哪个。

小技巧: 我习惯把 createContext 和对应的 Hook 封装成一个文件,这样引用起来特别干净。

Context 性能优化:别让所有组件都跟着重渲染

这是 Context 最容易被忽视的坑。你想想看:只要 Provider 的 value 变了,所有消费这个 Context 的子组件都会重新渲染,不管它们用到的数据有没有变。

我曾经在一个后台管理系统里,把用户信息、菜单权限、主题设置全塞进一个 Context。结果每次切换主题,整个页面所有组件都重渲染了一遍。卡得不行。

优化策略一:拆分 Context

把不相关的数据拆成不同的 Context。比如:

  • ThemeContext:只放主题相关
  • AuthContext:只放登录状态
  • LocaleContext:只放国际化语言

这样,切换主题时只有用到 ThemeContext 的组件会重渲染,其他组件纹丝不动。

优化策略二:用 useMemo 稳定 value

Provider 的 value 如果是一个对象字面量,每次父组件渲染都会生成一个新对象,导致所有子组件都跟着渲染。

// ❌ 每次渲染都生成新对象
<ThemeContext.Provider value={{ theme, setTheme }}>

// ✅ 用 useMemo 稳定引用
const contextValue = useMemo(() => ({ theme, setTheme }), [theme, setTheme]);
<ThemeContext.Provider value={contextValue}>

这个优化我几乎在每个项目里都会用。效果立竿见影。

优化策略三:组件拆分 + React.memo

如果一个组件只用了 Context 中的部分数据,但它的子组件没用到,可以用 React.memo 包裹子组件,避免不必要的渲染。

const ExpensiveChild = React.memo(function ExpensiveChild() {
  return <div>我很贵,别乱渲染我</div>;
});

function Parent() {
  const { theme } = useContext(ThemeContext);
  return (
    <div style={{ background: theme === 'dark' ? '#333' : '#fff' }}>
      <ExpensiveChild />
    </div>
  );
}
注意: React.memo 只对 props 做浅比较。如果 Context 的 value 每次都生成新对象,memo 也救不了你。所以 useMemo 是前提。

Context 使用场景速查表

场景 推荐用 Context? 替代方案
主题 / 国际化语言 ✅ 非常适合 —
用户登录状态 ✅ 适合 —
全局表单数据 ⚠️ 谨慎 状态管理库(Zustand / Redux)
高频更新的数据(如鼠标位置) ❌ 不推荐 事件监听 + 局部 state
深层组件传参(3层以上) ✅ 适合 组合组件(Component Composition)

Context 与状态管理库的关系

很多人问:有了 Context,是不是就不需要 Redux 了?

我的看法是:Context 适合「低频更新、全局共享」的数据。比如主题、语言、用户信息。但如果你的应用有大量高频交互、复杂的状态派生逻辑,还是得上专门的状态管理库。

Context 本质上只是一个「依赖注入」机制,它不帮你做状态管理。你仍然需要 useState / useReducer 来管理数据。而 Redux、Zustand 这些库,提供了更完善的状态变更追踪、中间件、DevTools 等能力。

一句话总结: 小项目用 Context + useReducer 完全够用。大项目、高频交互场景,考虑 Zustand 或 Redux Toolkit。

本章知识结构图

下面这张图帮你理清 Context 的核心脉络:

React Context 知识结构 React Context createContext Provider Consumer / useContext value 变化 → 所有订阅者重渲染 优化①:拆分 Context 优化②:useMemo 稳定 value 优化③:React.memo 隔离子组件 适合:主题 / 语言 / 用户信息 不适合:高频更新 / 复杂状态派生 替代:Zustand / Redux Toolkit 核心原则:Context 是依赖注入,不是状态管理 数据逻辑仍由 useState / useReducer 负责

避坑指南

  • 不要滥用 Context: 如果一个数据只在两个相邻组件间传递,用 props 就够了。Context 是为「跨多层、多组件」设计的。
  • Provider 不要嵌套太深: 我见过有人套了七八层 Provider,调试的时候根本分不清数据从哪来的。尽量控制在 3 层以内。
  • 默认值要合理: createContext 的默认值只在「没有 Provider」时生效。如果你确定某个 Context 必须有 Provider,可以把默认值设为 null,然后在 useContext 里做非空判断。
  • 测试时注意: 每个测试用例最好都包裹对应的 Provider,否则拿到的都是默认值,容易出假阳性。
我的习惯: 每次创建 Context,我都会同时导出一个自定义 Hook,比如 useTheme(),里面封装了 useContext 和非空判断。这样业务组件里只用调用 useTheme(),清爽又安全。