讲解

props 传值隔代就痛苦:主题、语言、登录用户这种「全应用都关心」的数据,逐层透传四五层,中间组件全成了快递员。Context 解决这个问题:顶层组件用 Provider 提供值,任意深度的后代用 useContext 直接消费,中间层零感知。

用法三步:const ThemeContext = createContext(defaultValue) 创建;<ThemeContext.Provider value={theme}> 在祖先组件提供;const theme = useContext(ThemeContext) 在任意后代消费。Provider 的 value 变化时,所有消费组件自动重渲染。createContext 的默认值只在「组件上方没有任何 Provider」时生效——传 null 给 Provider 并不会用默认值兜底,这是个著名误解。

Context 的适用边界要划清。适合:低频变化、真正全局的数据(主题、语言、当前用户、路由信息)。不适合:高频变化的状态(每次 value 变化所有消费者重渲染,购物车这种每点一次全树刷新的场景要用专门的状态管理库)。工程实践:value 用 useMemo 包住,避免父组件每次渲染都创建新对象导致消费者白白重渲染;多个关注点拆多个 Context,别把全应用数据塞一个大 Context。

Context 不是状态管理工具——它只是「传值管道」,状态本身还是存在某个组件的 useState/useReducer 里。「useReducer + Context」组合起来就是一个迷你 Redux,这是官方文档认可的中型应用方案。

示例

模拟 Context 的就近查找与「默认值只在无 Provider 时生效」(在本教程构建时被真实执行):

import assert from 'node:assert/strict';

// 迷你 Context:沿组件树向上找最近的 Provider
function createContext(defaultValue) {
  return { defaultValue, providers: new WeakMap() };
}

function createNode(name, parent = null) {
  return { name, parent, provided: new Map() };
}
function provide(node, context, value) {
  node.provided.set(context, value);
}
function useContext(node, context) {
  let cur = node;
  while (cur) {
    if (cur.provided.has(context)) return cur.provided.get(context);
    cur = cur.parent;
  }
  return context.defaultValue; // 没有 Provider 才用默认值
}

const ThemeContext = createContext('light');
const UserContext = createContext(null);

// 树:App(提供 theme=dark) → Layout(提供 theme=light 局部覆盖) → Page → Button
const app = createNode('App');
const layout = createNode('Layout', app);
const page = createNode('Page', layout);
const button = createNode('Button', page);

provide(app, ThemeContext, 'dark');
provide(layout, ThemeContext, 'light'); // 就近原则:覆盖上层

assert.strictEqual(useContext(button, ThemeContext), 'light'); // 最近的 Provider 赢
assert.strictEqual(useContext(button, UserContext), null); // 无 Provider → 默认值

// Provider value={null} ≠ 默认值兜底
const Theme2 = createContext('light');
provide(page, Theme2, null);
assert.strictEqual(useContext(button, Theme2), null); // 拿到的是 null,不是 'light'!

// value 对象要稳定:每次新建对象会导致消费者重渲染
let renders = 0;
const stableValue = { theme: 'dark' };
function consume(value) {
  renders++;
  return value;
}
consume(stableValue);
consume(stableValue); // 引用相同 → 不需要重渲染
assert.strictEqual(Object.is(stableValue, stableValue), true);

console.log('Button 读到主题:', useContext(button, ThemeContext), '(就近 Provider 覆盖上层)');
console.log('Provider value={null} 时读到:', useContext(button, Theme2), '(默认值不兜底)');

真实代码:

import { createContext, useContext, useMemo, useState } from 'react';

const ThemeContext = createContext('light');

function App() {
  const [theme, setTheme] = useState('light');
  // value 用 useMemo 稳定引用,避免消费者白重渲染
  const value = useMemo(() => ({ theme, toggle: () => setTheme((t) => (t === 'light' ? 'dark' : 'light')) }), [theme]);
  return (
    <ThemeContext.Provider value={value}>
      <Page />
    </ThemeContext.Provider>
  );
}

function DeepButton() {
  const { theme, toggle } = useContext(ThemeContext); // 任意深度直接消费
  return <button className={theme} onClick={toggle}>当前:{theme}</button>;
}

常见坑

  • 以为 createContext 默认值能兜底 null:默认值只在「上方无 Provider」时生效;Provider value={null} 就给你 null。
  • value 传内联对象value={{ theme }} 每次渲染新引用,所有消费者全部重渲染;useMemo 稳定它。
  • 高频状态进 Context:输入框每敲一键全树刷新;高频状态用状态管理库或状态下沉。
  • 一个 Context 塞所有数据:任何字段变化全体消费者重渲染;按关注点拆多个 Context。
  • 用 Context 做组件配置滥用:只给一两个组件用的数据 props 更直白,别逢传值必 Context。

小结

Context = Provider 提供 + useContext 消费的传值管道;就近覆盖、默认值只在无 Provider 时生效;value 要稳定、按关注点拆分。下一章管理复杂状态逻辑:useReducer。