React: Context 与全局状态

最后更新:2026-10-26

1. 你将学到



2. 一个主题切换的故事

(1) 痛点:Props 穿透 5 层组件

Alice 要给应用加一个"黑暗模式"切换功能。数据流要穿透 5 层组件:

▶ 示例:Context使用

输出:

TEXT
基于 Context 的状态在组件之间共享
▶ 试一试

输出:

TEXT
App 组件的状态为 'light'。渲染可交互界面。

Alice 的问题:

(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

100%
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。

📖 小节


📝 作业

  1. 基础题(难度⭐):创建一个 LocaleProvider + useLocale Hook,提供 locale(zh/en)和 toggleLocale() 方法,实现中英文切换。
  2. 进阶题(难度⭐⭐):创建一个 AuthProvider + useAuth Hook,提供 user、login(username, password)、logout() 方法,管理用户认证状态。
  3. 挑战题(难度⭐⭐⭐):创建一个 CartProvider + useCart Hook,使用 Context + useReducer 实现购物车,包含添加、删除、修改数量、清空功能,用 useMemo 优化避免不必要重渲染。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏