第四章:Context API 基础
好,咱们今天聊聊 Context API。说实话,这是 React 状态管理里一个「承上启下」的东西。你如果只会 props 一层层传,那叫「props drilling」——说白了就是钻洞,代码又臭又长。Context API 就是来救场的。
4.1 React.createContext —— 先挖个坑
要理解 Context,你得先知道它是个「容器」。这个容器用来装数据,然后让整个组件树都能访问到。
怎么创建?很简单:
const MyContext = React.createContext(defaultValue);
这里的 defaultValue 是啥?是当组件没有匹配到 Provider 时用的默认值。我个人习惯给它一个有意义的值,比如空对象或者 null,方便调试。
我在项目中遇到过一个问题:有人把 defaultValue 设成了 undefined,结果组件里一取值就报错。嗯,这里要注意——defaultValue 最好别偷懒。
4.2 Provider 与 Consumer —— 一个给,一个拿
创建好 Context 之后,你需要两个东西:Provider 和 Consumer。
Provider 是「数据提供方」。它包裹在组件树的最外层,把数据往下传:
<MyContext.Provider value={{ user: '张三', age: 25 }}>
<App />
</MyContext.Provider>
Consumer 是「数据消费方」。它写在子组件里,用来拿数据:
<MyContext.Consumer>
{value => <div>用户:{value.user}</div>}
</MyContext.Consumer>
你想想看,Consumer 的写法其实挺啰嗦的。尤其是嵌套多个 Context 的时候,代码会变成「回调地狱」。我早期写项目时就被这个坑过——三层 Context 嵌套,代码直接没法看。
4.3 useContext 钩子 —— 真香
React 16.8 之后,Hooks 来了。useContext 就是 Consumer 的「平替」,而且更好用。
用法就三步:
- 创建 Context
- 用 Provider 包裹
- 在子组件里调用 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%。
4.4 避坑指南
讲几个我踩过的坑:
- Provider 的 value 不要直接传对象字面量——每次渲染都会创建新对象,导致子组件不必要的重渲染。我建议用 useMemo 或者 useState 来管理。
- 不要滥用 Context——它适合「全局性」的数据,比如主题、用户信息、语言设置。如果只是父子组件传值,props 就够了。
- 多个 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 会导致整个组件树重渲染——性能会崩。
4.7 小结
Context API 是 React 状态管理的基础工具。它解决了 props drilling 的问题,让数据传递变得优雅。useContext 钩子更是让代码简洁了不少。
但记住:Context 适合「全局但低频」的数据。别什么状态都往里塞,否则你会后悔的。
下一章咱们聊聊 useReducer——它和 Context 搭配起来,能解决更复杂的状态管理问题。