讲解
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。