React: Context 与全局状态
最后更新:2026-10-26
1. 你将学到
- createContext 和 Provider
- useContext 消费上下文
- Context vs Props Drilling
- 多层嵌套和性能优化
- Context + useReducer 组合(轻量状态管理)
2. 一个主题切换的故事
(1) 痛点:Props 穿透 5 层组件
Alice 要给应用加一个"黑暗模式"切换功能。数据流要穿透 5 层组件:
▶ 示例:Context使用
输出:
TEXT
基于 Context 的状态在组件之间共享
输出:
TEXT
App 组件的状态为 'light'。渲染可交互界面。
Alice 的问题:
theme从 App 一层层传到 Button,中间 3 层其实不需要这个数据- 如果再加一个用户信息要传递,Props 会变得极其臃肿
- 这就是经典的 Props Drilling(属性钻取)
(2) Context 的解法
▶ 试一试收益:中间组件(Page、MainLayout、Sidebar)不再需要传 theme Props,数据直接从 Provider 跳到 Consumer。
3. Context 的核心概念
| API | 作用 | 参数/返回值 |
|---|---|---|
createContext(defaultValue) |
创建上下文容器 | 返回 { Provider, Consumer } |
<Context.Provider value={...}> |
提供数据给子树 | value:任意类型,变化时触发 Consumer 重渲染 |
useContext(Context) |
消费最近的 Provider 数据 | 返回 Provider 的 value,无 Provider 返回默认值 |
自定义 useXxx() Hook |
封装 useContext + 错误检查 | 推荐模式,Provider 缺失时抛出明确错误 |
(1) createContext + Provider + useContext
graph TB
A[createContext<br/>创建上下文容器] --> B[Provider<br/>包裹组件树<br/>提供 value]
B --> C[中间组件<br/>不关心 Context]
C --> D[useContext<br/>消费 Context 数据]
style A fill:#1890ff,color:#fff
style B fill:#52c41a,color:#fff
style D fill:#ff6b6b,color:#fff
(2) Context 的适用场景
| 适合用 Context | 不适合用 Context |
|---|---|
| 主题(暗黑模式) | 组件配置参数(用 Props) |
| 用户登录信息 | 表单输入值(用本地 State) |
| 语言/国际化 | API 数据(用 TanStack Query) |
| 路由状态 | 动画状态(用动画库管理) |
| 全局设置 | 简单父传子(直接用 Props) |
规则:数据需要被"多个层级、多个组件"共享时才用 Context。如果只是父子组件传值,Props 就够。
4. Context 嵌套和 Provider 组件化
(1) 多层 Context 嵌套
实际项目通常有多个 Context:
JSX
function App() {
return (
<AuthProvider>
<ThemeProvider>
<I18nProvider>
<NotificationProvider>
<MainApp />
</NotificationProvider>
</I18nProvider>
</ThemeProvider>
</AuthProvider>
)
}
为了避免嵌套地狱,可以写一个 AppProviders 组合组件:
JSX
function AppProviders({ children }) {
return (
<AuthProvider>
<ThemeProvider>
<I18nProvider>
<NotificationProvider>
{children}
</NotificationProvider>
</I18nProvider>
</ThemeProvider>
</AuthProvider>
)
}
function App() {
return (
<AppProviders>
<MainApp />
</AppProviders>
)
}
(2) 自定义 Provider Hook
▶ 试一试5. Context 性能优化
Context 的值变化时,所有消费该 Context 的组件都会重新渲染。这是 Context 的最大性能陷阱。
| 优化方式 | 原理 | 适用场景 |
|---|---|---|
useMemo 稳定 value |
避免每次渲染创建新对象引用 | Provider 的 value 包含函数或对象时 |
| 拆分 Context | "变化"与"不变"数据分到不同 Context | 部分 Consumer 只需要稳定数据 |
React.memo 包裹 Consumer |
跳过 props 未变的重渲染 | Consumer 组件接收其他 props 时 |
| 选择性订阅 | 用 selector 只取需要的切片 | 配合 Zustand/Redux 实现精确更新 |
JSX
// ❌ 问题:每次 App 渲染,value 都是新对象 → 所有 Consumer 都重渲染
function App() {
const [count, setCount] = useState(0)
return (
<ThemeContext.Provider value={{ theme: 'dark', setCount }}>
<ExpensiveTree />
</ThemeContext.Provider>
)
}
// ✅ 修复 1:用 useMemo 稳定 value 引用
function App() {
const [count, setCount] = useState(0)
const themeValue = useMemo(
() => ({ theme: 'dark', toggleTheme: () => setCount(c => c + 1) }),
[]
)
return (
<ThemeContext.Provider value={themeValue}>
<ExpensiveTree />
</ThemeContext.Provider>
)
}
// ✅ 修复 2:把"不变"和"变"的数据分开不同的 Context
const SettingsContext = createContext({ theme: 'light', language: 'zh' })
const CountContext = createContext(0)
function App() {
const [count, setCount] = useState(0)
return (
<SettingsContext.Provider value={settingsValue}> {/* useMemo 稳定 */}
<CountContext.Provider value={count}>
<ExpensiveTree />
</CountContext.Provider>
</SettingsContext.Provider>
)
}
// 只有用到 CountContext 的组件会在 count 变化时重渲染
// SettingsContext 的消费者不会受影响
6. Context + useReducer:构建轻量全局状态
结合 Context 的"跨层级传递"和 useReducer 的"复杂状态管理",可以构建一个轻量级全局状态方案:
▶ 展开运行示例(125 行)
输出:
TEXT
显示:"Button"。状态:theme(setter:setTheme)。按钮:Button
预期输出:完整通知系统。顶部有通知铃铛(显示未读数),中间可以发 info/success/error 三种通知,下方显示通知列表,每条通知可标记已读或删除。
▶ 示例 2:用户认证状态管理(Context + 自定义 Hook)
▶ 展开运行示例(114 行)
输出:
TEXT
邮箱/密码登录表单。邮箱格式无效 → 红色边框 + "The email address format is incorrect."。密码不足 6 位 → "Password must be at least 6 chars"。提交 → console.log({email, password})
输出:
TEXT
副标题:"🔐 Log In"。显示:"🔐 Log In"。状态:user(setter:setUser)、loading(setter:setLoading)、username(setter:setUsername)。按钮:{loading ? 'Logging in......' : 'Log In'}、Log Out。输入:Username(Input admin Experience the Role of an Administrator)、Password。带提交处理的表单。useMemo 优化计算。useCallback 缓存处理函数引用。Context 提供全局状态。异步数据获取/加载状态。基于定时器的行为
▶ 示例 3:多层 Provider 组合与 i18n 国际化
▶ 试一试输出:
TEXT
国际化:语言切换器改变所有文本。Context + provider 模式支持多语言
输出:
TEXT
副标题:"{t('greeting')}"。显示:"Chinese"。状态:locale(setter:setLocale)。下拉框:Chinese、English。useMemo 优化计算。useCallback 缓存处理函数引用。Context 提供全局状态
▶ 示例 4:拆分 Context 优化性能
▶ 试一试输出:
TEXT
SplitContextApp 组件的状态为:null、false。渲染可交互界面。
输出:
TEXT
显示:"Login"。状态:user(setter:setUser)、loading(setter:setLoading)。按钮:login('Alice')}>Login。useMemo 优化计算。Context 提供全局状态。异步数据获取/加载状态。基于定时器的行为
▶ 示例 5:useContext 替代 Props Drilling 完整示例
▶ 试一试输出:
TEXT
SettingsApp 组件的状态为:{
theme: 'light', fontSize: 14, sidebarCollapsed: false,
}。渲染可交互界面。
❓ 常见问题
Q Context 的 value 每次都是新对象,导致所有 Consumer 都重渲染怎么办?
A 用
useMemo 稳定 value 引用:const value = useMemo(() => ({ theme, toggleTheme }), [theme])。还可以把"稳定的"和"变化的"数据分到不同的 Context 中。如果优化后仍然卡顿,考虑用 Zustand(它自带精确更新,不会触发无关组件的重渲染)。Q Context 嵌套太多层了怎么办?
A ① 用 Provider 组合组件(如前面的
AppProviders)把嵌套封装起来;② 思考是否真的需要那么多 Context——有些"全局"数据其实只是"局部全局"(比如某个页面的设置),不需要放在 App 顶层的 Context 中。Q 自定义 useXxx Hook 里为什么要检查 context 是否为 null?
A 这是一个防御性编程技巧。如果开发者忘记用 Provider 包裹组件就使用 useContext,context 会是 null(如果 createContext 没有传入默认值)或产生难以调试的错误。提前抛出清晰的错误信息,比用户在浏览器控制台看到 "Cannot read property of null" 要友好得多。
Q Context 可以替代 Redux 吗?
A 小到中型项目可以,大型项目不行。Context 缺少 Redux 的三个能力:① 中间件机制(日志、异步、持久化);② 时间旅行调试(DevTools 回放状态变化);③ 选择性订阅(Context value 变化导致所有 Consumer 重渲染,Redux 可以用 selector 只订阅需要的切片)。如果你的项目需要这三个能力,用 Zustand(轻量)或 Redux Toolkit。
📖 小节
createContext→.Provider→useContext三步使用 Context- Context 解决 Props Drilling,但不解决"状态管理"问题
- 性能陷阱:Context value 变化会导致所有 Consumer 重渲染,用
useMemo优化 - 自定义
useXxxHook + 错误检查是推荐的最佳实践 Context + useReducer可以作为轻量级全局状态方案,但复杂场景还是用 Zustand/Redux
📝 作业
- 基础题(难度⭐):创建一个
LocaleProvider+useLocaleHook,提供locale(zh/en)和toggleLocale()方法,实现中英文切换。 - 进阶题(难度⭐⭐):创建一个
AuthProvider+useAuthHook,提供user、login(username, password)、logout()方法,管理用户认证状态。 - 挑战题(难度⭐⭐⭐):创建一个
CartProvider+useCartHook,使用 Context + useReducer 实现购物车,包含添加、删除、修改数量、清空功能,用useMemo优化避免不必要重渲染。