第四章:Context API 基础

好,咱们今天聊聊 Context API。说实话,这是 React 状态管理里一个「承上启下」的东西。你如果只会 props 一层层传,那叫「props drilling」——说白了就是钻洞,代码又臭又长。Context API 就是来救场的。

4.1 React.createContext —— 先挖个坑

要理解 Context,你得先知道它是个「容器」。这个容器用来装数据,然后让整个组件树都能访问到。

怎么创建?很简单:

const MyContext = React.createContext(defaultValue);

这里的 defaultValue 是啥?是当组件没有匹配到 Provider 时用的默认值。我个人习惯给它一个有意义的值,比如空对象或者 null,方便调试。

小提示:defaultValue 只在组件没有被 Provider 包裹时生效。一旦用了 Provider,defaultValue 就被覆盖了。

我在项目中遇到过一个问题:有人把 defaultValue 设成了 undefined,结果组件里一取值就报错。嗯,这里要注意——defaultValue 最好别偷懒。

4.2 Provider 与 Consumer —— 一个给,一个拿

创建好 Context 之后,你需要两个东西:ProviderConsumer

Provider 是「数据提供方」。它包裹在组件树的最外层,把数据往下传:

<MyContext.Provider value={{ user: '张三', age: 25 }}>
  <App />
</MyContext.Provider>

Consumer 是「数据消费方」。它写在子组件里,用来拿数据:

<MyContext.Consumer>
  {value => <div>用户:{value.user}</div>}
</MyContext.Consumer>

你想想看,Consumer 的写法其实挺啰嗦的。尤其是嵌套多个 Context 的时候,代码会变成「回调地狱」。我早期写项目时就被这个坑过——三层 Context 嵌套,代码直接没法看。

警告:Consumer 的写法在函数组件里已经很少用了。除非你维护老项目,否则建议直接用 useContext。

4.3 useContext 钩子 —— 真香

React 16.8 之后,Hooks 来了。useContext 就是 Consumer 的「平替」,而且更好用。

用法就三步:

  1. 创建 Context
  2. 用 Provider 包裹
  3. 在子组件里调用 useContext

代码长这样:

// 1. 创建
const ThemeContext = React.createContext('light');

// 2. 在父组件里提供
function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

// 3. 在子组件里消费
function Toolbar() {
  const theme = useContext(ThemeContext);
  return <div>当前主题:{theme}</div>;
}

看到了吗?一行 useContext 就搞定了。不用再写那些嵌套的回调函数了。我曾经在一个中型项目里,把所有 Consumer 都改成了 useContext,代码量直接少了 30%。

核心要点:useContext 接收一个 Context 对象,返回当前 Context 的值。当 Provider 的 value 变化时,所有用到这个 Context 的组件都会重新渲染。

4.4 避坑指南

讲几个我踩过的坑:

  • Provider 的 value 不要直接传对象字面量——每次渲染都会创建新对象,导致子组件不必要的重渲染。我建议用 useMemo 或者 useState 来管理。
  • 不要滥用 Context——它适合「全局性」的数据,比如主题、用户信息、语言设置。如果只是父子组件传值,props 就够了。
  • 多个 Context 可以嵌套——但别超过三层。我曾经见过一个项目,Context 嵌套了五层,调试起来简直噩梦。
我的习惯:把 Context 相关的代码单独抽成一个文件,比如 ThemeContext.js。这样结构清晰,也方便复用。

4.5 实战:一个简单的主题切换

咱们来写个完整的例子。假设你要做一个主题切换功能:

// ThemeContext.js
import React, { createContext, useState, useContext } from 'react';

const ThemeContext = createContext('light');

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const toggleTheme = () => setTheme(prev => prev === 'light' ? 'dark' : 'light');

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

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme 必须在 ThemeProvider 内使用');
  }
  return context;
}

然后在组件里用:

function App() {
  return (
    <ThemeProvider>
      <Header />
      <Content />
    </ThemeProvider>
  );
}

function Header() {
  const { theme, toggleTheme } = useTheme();
  return (
    <header style={{ background: theme === 'light' ? '#fff' : '#333' }}>
      <button onClick={toggleTheme}>切换主题</button>
    </header>
  );
}

这个模式我用了很多次。把 Context 的逻辑封装成自定义 Hook,调用方只需要关心 useTheme() 返回了什么,不用管底层实现。说白了,这就是「关注点分离」。

4.6 什么时候用 Context?

我总结了一个简单的判断标准:

场景 推荐方案 原因
父子组件传值 props 简单直接,不需要 Context
跨多层组件传值 Context 避免 props drilling
全局状态(主题、用户) Context + useReducer 适合低频更新的全局数据
高频更新的全局状态 Redux 或 Zustand Context 在高频更新时性能不佳

嗯,这里要注意:Context 不是万能的。它适合「低频更新」的全局数据。如果你有个计数器每秒变一次,用 Context 会导致整个组件树重渲染——性能会崩。

我曾经踩过的坑:在一个实时聊天项目里,用 Context 管理消息列表。结果每条消息进来,整个聊天界面都重渲染。后来改用 Redux + 选择性订阅才解决。

4.7 小结

Context API 是 React 状态管理的基础工具。它解决了 props drilling 的问题,让数据传递变得优雅。useContext 钩子更是让代码简洁了不少。

但记住:Context 适合「全局但低频」的数据。别什么状态都往里塞,否则你会后悔的。

下一章咱们聊聊 useReducer——它和 Context 搭配起来,能解决更复杂的状态管理问题。