一句话定义
Context 让一棵子树里的所有组件直接读取顶层提供的数据(createContext + <Provider> + useContext),不用逐层传 props——代价是消费组件会在 context 值变化时全部重渲染。
为什么重要
它解决了 prop drilling(穿层传递)这个结构性痛点;但它又是最容易被滥用的 API——「把整个应用状态都塞进一个大 Context」是反模式,会带来大面积重渲染与测试负担。学会「何时用、怎么切分」是核心目标。
前置知识
核心概念
- createContext(defaultValue):defaultValue 仅当树上没有 Provider 时生效(常用于让测试无需 Provider)。
- Provider:把 value 提供给子树;value 变化 → 所有消费组件重渲染。
- useContext(MyContext):在组件(或自定义 Hook)内读取最近 Provider 的 value。
- 适用形态:低频变化、全局横切的「环境」:主题、当前语言、当前登录用户、路由实例。不是高频状态的车(高频用状态库,kp-021)。
- 典型配套:Provider 里持有 state,value 里同时提供「状态 + setter」。
原理 / 机制
基础三件套:
jsxconst ThemeContext = createContext(null);
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
// value 引用稳定性重要:内联对象字面量会让每次渲染都"变化"
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
}
function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error('useTheme 必须在 <ThemeProvider> 内使用');
return ctx;
}
// 任意深度的叶子直接用
function ThemeButton() {
const { theme, setTheme } = useTheme();
return <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>{theme}</button>;
}
重渲染链路:theme 变 → Provider 重渲染 → 新 value(useMemo 后引用稳定)→ 所有 useContext(ThemeContext) 的组件重渲染;未消费的中间层不受影响。若 value 不加 useMemo,{ theme, setTheme } 每次都是新对象 → 所有消费者在 Provider 的每次渲染都重渲染。
切分策略——「按变化频率与消费群体拆分 Provider」:
jsx// ✅ 拆分:主题变化只惊动主题消费者
<AuthProvider> {/* 用户信息,登录才变 */}
<ThemeContext.Provider> {/* 主题,偶尔变 */}
<RouterProvider> {/* 路由,每次跳转变 */}
<App />
</RouterProvider>
</ThemeContext.Provider>
</AuthProvider>
// ❌ 反模式:一个 AppStateContext 包罗万象 → 任何字段变化全树重渲染
直观类比
Context 是楼层公示栏:物业(Provider)贴公告,全楼住户(消费者)直接看,不需要前台逐层转达(prop drilling)。但每次公告换内容,整楼都会凑过来看一眼(重渲染)——所以重要但少变的「物业规章」(主题/语言)适合公示栏;高频变动的「电梯实时楼层」(每秒变的状态)贴公示栏只会让人群不停聚集,应该装个电梯显示屏(状态库/局部状态)。
实例 / 案例
「当前用户」的规范用法(读为主、写为事件):
jsxconst UserContext = createContext(null);
export function UserProvider({ children }) {
const [user, setUser] = useState(null);
useEffect(() => { fetchCurrentUser().then(setUser); }, []);
const value = useMemo(
() => ({ user, logout: () => { setUser(null); api.logout(); } }),
[user]
);
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
}
export function useUser() { return useContext(UserContext); }
// 任意组件:
function Avatar() {
const { user } = useUser();
if (!user) return null;
return <img src={user.avatarUrl} alt={user.name} />;
}
自带的 useUser()(而非散落各处的 useContext(UserContext))带来三件事:错误提示、未来可替换实现、语义化。
常见误区
- 拿 Context 当全局状态库:高频变化(鼠标位置、滚动、倒计时)放进 context → 性能雪崩。React 官方明确:Context 用于「低频变化的环境数据」。
- value 内联对象:
value={{ a, b }}每次渲染新引用,消费者全体陪跑;用 useMemo 或拆分 state。 - 忘了 defaultValue 的语义:有 Provider 时它不生效;测试时「不包 Provider 得到默认值」是设计能力不是巧合。
- 消费组件在 Provider 上游:读到的永远是 defaultValue(通常 null);把 Provider 放到尽可能高的正确位置。
- 一个巨型 Context 对象:既难测试又难优化;按领域拆 Provider + 自定义 Hook。
自测题
- 写出「创建 → 提供三层 → 消费」的最小 Context 代码,并在消费 Hook 里做防护。
- 为什么 value 需要 useMemo?不加会发生什么级别的重渲染?
- 「鼠标坐标」适合放 Context 吗?给出判断依据和替代方案。
- 拆分 Provider 的两个原则是什么(变化频率 / 消费群体),各举一例。
与其他知识点的关系
- 向前:替代 kp-004 的 drilling;实现上依赖 kp-005 state 与 kp-018 useMemo。
- 向后:kp-019 会量化「context 变化引发的重渲染」在性能分析里的表现;kp-021 讲 Context 与 Zustand/Redux 的分工;kp-022 指出并发时代 context 更新同样受优先级调度。
延伸阅读
- Passing Data Deeply with Context:<https://react.dev/learn/passing-data-deeply-with-context>
- Scaling Up with Reducer and Context(官方组合教程):<https://react.dev/learn/scaling-up-with-reducer-and-context>