4、React中的useContext:跨组件状态共享、Provider与Consumer、Context性能优化
讲完 useReducer,咱们来聊聊另一个日常高频使用的 Hook —— useContext。
说实话,我刚接触 React 那会儿,对 Context 的理解特别浅。就觉得它是个「全局变量」,哪都能用。结果项目一复杂,性能问题全冒出来了。后来踩了不少坑,才慢慢摸清楚它的脾气。
今天这一章,我就把 Context 的来龙去脉、最佳实践、还有那些容易翻车的地方,一次性给你讲透。
为什么需要 Context?
先想一个问题:如果有个「用户登录信息」,需要在页面最顶层的导航栏、中间的侧边栏、底部的个人中心都用上,你怎么传?
用 props 一层层往下传?那叫「prop drilling」。组件树深一点,中间那些不关心这个数据的组件,也得帮着传一遍。代码又臭又长,改起来还容易漏。
Context 就是来解决这个问题的。它让你能把数据「注入」到组件树的某个层级,然后下面所有子组件都能直接读取,不用经过中间层。
Provider 与 Consumer:两代写法
Context 的使用分两派:老派的 Consumer 模式,和新派的 useContext Hook。我建议你直接学新的,但老代码里经常见到 Consumer,所以也得认识一下。
1. 创建 Context
const ThemeContext = React.createContext('light');
createContext 接受一个默认值。这个默认值只在「没有 Provider 包裹」时生效。
2. Provider:数据的源头
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Toolbar />
</ThemeContext.Provider>
);
}
Provider 的 value 变了,所有消费这个 Context 的子组件都会重新渲染。嗯,这里就是性能问题的根源,后面细说。
3. Consumer:老式写法
function Toolbar() {
return (
<ThemeContext.Consumer>
{({ theme, setTheme }) => (
<button onClick={() => setTheme('light')}>
当前主题:{theme}
</button>
)}
</ThemeContext.Consumer>
);
}
Consumer 用了一个「render props」模式。说实话,嵌套多了可读性很差。我个人很少用这种写法。
4. useContext:现代写法
function Toolbar() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button onClick={() => setTheme('light')}>
当前主题:{theme}
</button>
);
}
简洁多了,对吧?useContext 直接返回 Context 的当前值,想用哪个字段就用哪个。
Context 性能优化:别让所有组件都跟着重渲染
这是 Context 最容易被忽视的坑。你想想看:只要 Provider 的 value 变了,所有消费这个 Context 的子组件都会重新渲染,不管它们用到的数据有没有变。
我曾经在一个后台管理系统里,把用户信息、菜单权限、主题设置全塞进一个 Context。结果每次切换主题,整个页面所有组件都重渲染了一遍。卡得不行。
优化策略一:拆分 Context
把不相关的数据拆成不同的 Context。比如:
- ThemeContext:只放主题相关
- AuthContext:只放登录状态
- LocaleContext:只放国际化语言
这样,切换主题时只有用到 ThemeContext 的组件会重渲染,其他组件纹丝不动。
优化策略二:用 useMemo 稳定 value
Provider 的 value 如果是一个对象字面量,每次父组件渲染都会生成一个新对象,导致所有子组件都跟着渲染。
// ❌ 每次渲染都生成新对象
<ThemeContext.Provider value={{ theme, setTheme }}>
// ✅ 用 useMemo 稳定引用
const contextValue = useMemo(() => ({ theme, setTheme }), [theme, setTheme]);
<ThemeContext.Provider value={contextValue}>
这个优化我几乎在每个项目里都会用。效果立竿见影。
优化策略三:组件拆分 + React.memo
如果一个组件只用了 Context 中的部分数据,但它的子组件没用到,可以用 React.memo 包裹子组件,避免不必要的渲染。
const ExpensiveChild = React.memo(function ExpensiveChild() {
return <div>我很贵,别乱渲染我</div>;
});
function Parent() {
const { theme } = useContext(ThemeContext);
return (
<div style={{ background: theme === 'dark' ? '#333' : '#fff' }}>
<ExpensiveChild />
</div>
);
}
Context 使用场景速查表
| 场景 | 推荐用 Context? | 替代方案 |
|---|---|---|
| 主题 / 国际化语言 | ✅ 非常适合 | — |
| 用户登录状态 | ✅ 适合 | — |
| 全局表单数据 | ⚠️ 谨慎 | 状态管理库(Zustand / Redux) |
| 高频更新的数据(如鼠标位置) | ❌ 不推荐 | 事件监听 + 局部 state |
| 深层组件传参(3层以上) | ✅ 适合 | 组合组件(Component Composition) |
Context 与状态管理库的关系
很多人问:有了 Context,是不是就不需要 Redux 了?
我的看法是:Context 适合「低频更新、全局共享」的数据。比如主题、语言、用户信息。但如果你的应用有大量高频交互、复杂的状态派生逻辑,还是得上专门的状态管理库。
Context 本质上只是一个「依赖注入」机制,它不帮你做状态管理。你仍然需要 useState / useReducer 来管理数据。而 Redux、Zustand 这些库,提供了更完善的状态变更追踪、中间件、DevTools 等能力。
本章知识结构图
下面这张图帮你理清 Context 的核心脉络:
避坑指南
- 不要滥用 Context: 如果一个数据只在两个相邻组件间传递,用 props 就够了。Context 是为「跨多层、多组件」设计的。
- Provider 不要嵌套太深: 我见过有人套了七八层 Provider,调试的时候根本分不清数据从哪来的。尽量控制在 3 层以内。
- 默认值要合理: createContext 的默认值只在「没有 Provider」时生效。如果你确定某个 Context 必须有 Provider,可以把默认值设为 null,然后在 useContext 里做非空判断。
- 测试时注意: 每个测试用例最好都包裹对应的 Provider,否则拿到的都是默认值,容易出假阳性。