第11章 useContext与跨组件传值

Chapter-11 - useContext 与跨层级数据传递

11.1 Context 基础

11.1.1 Props 层层传递的痛苦:Props Drilling

在第六章我们学过,数据通过 props 从父组件传给子组件。但如果组件嵌套得很深(层级很多),数据就需要一层一层地往下传——这叫 Props Drilling(props 层层向下钻)。

Props Drilling 有多痛苦?看看这个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 假设 App 是根组件,DeepChild 是深度为 5 层的组件
// App 有用户信息,想传给 DeepChild

function App() {
  const user = { name: '小明', avatar: '/avatar.jpg', role: 'admin' }
  return <Level1 user={user} />
}

function Level1({ user }) {
  return <Level2 user={user} />
}

function Level2({ user }) {
  return <Level3 user={user} />
}

function Level3({ user }) {
  return <Level4 user={user} />
}

function Level4({ user }) {
  return <Level5 user={user} />
}

function Level5({ user }) {
  // DeepChild 终于拿到了 user,但中间 4 层组件根本不需要 user!
  // 它们只是"中转站",被迫接收并传递 user
  return <div>{user.name}</div>
}

中间那些组件(Level1~Level4)根本不需要 user,但因为数据要往下传,它们被迫当"搬运工"。这不仅代码冗余,还会让组件之间的关系变得混乱。

11.1.2 Context 的解决思路:跨层级直接传递

Context(上下文) 就是用来解决这个问题的!它允许数据从父组件直接传递到任意深度的子组件,不需要经过中间每一层

1
2
3
// Context 的思路:
// 数据像"喷泉"一样,从父组件直接往下"浇灌"
// 中间的组件不需要知道这些数据的存在
flowchart TD
    A["App(数据源头)\n提供 Theme / User"] --> B["Level1(路过)"]
    B --> C["Level2(路过)"]
    C --> D["Level3(路过)"]
    D --> E["DeepChild(直接拿到数据)"]

    subgraph "Props Drilling 模式"
        A -.->|"props.user| user"| B
        B -.->|"props.user| user"| C
        C -.->|"props.user| user"| D
        D -.->|"props.user| user"| E
    end

    subgraph "Context 模式"
        A --"ThemeContext.Provider\nUserContext.Provider"--> B
        B --"直接路过,不需要传"| C
        C --"直接路过,不需要传"| D
        D --"直接路过,不需要传"| E
    end

使用 Context 的三步走:

  1. createContext() 创建一个"数据发射站"
  2. Provider 在父组件中包裹子树,把数据"发射"出去
  3. 在需要数据的子组件里,用 useContext() “接收"数据

对比一下:Props Drilling 就像老式电话,需要一级一级转接;而 Context 就是对讲机,父组件直接喊话,任意深度的子组件都能听到!📻


11.2 createContext 与 useContext

11.2.1 createContext 创建上下文

在数据共享之前,需要先用 createContext 创建一个"数据管道”。可以把它想象成一根自来水管——createContext 是买水管,Provider 是打开水龙头,useContext 是把水接到自己家。三步缺一不可。

createContext 的参数是默认值——当没有任何 Provider 包裹组件时,就会使用这个默认值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { createContext } from 'react'

// createContext() 创建一个 Context 对象
// 参数是默认值:当没有 <Provider> 包裹时,组件会使用这个默认值
const ThemeContext = createContext({
  theme: 'light',
  toggleTheme: () => {}
})

// 创建用户上下文
const UserContext = createContext(null)

// 导出供其他组件使用
export { ThemeContext, UserContext }

11.2.2 Provider 包裹:传递数据

Provider(提供者) 是 Context 的"发射塔"——用它包裹子组件,子组件就能收到 Context 提供的数据。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useState } from 'react'
import { ThemeContext, UserContext } from './contexts'

function App() {
  const [theme, setTheme] = useState('light')
  const user = { name: '小明', avatar: '/avatar.jpg' }

  function toggleTheme() {
    setTheme(prev => prev === 'light' ? 'dark' : 'light')
  }

  return (
    // 用 Provider 包裹子树
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      <UserContext.Provider value={user}>
        {/* 所有的子组件都能访问 ThemeContext 和 UserContext */}
        <div className={`app ${theme}`}>
          <Header />
          <Content />
          <Footer />
        </div>
      </UserContext.Provider>
    </ThemeContext.Provider>
  )
}

11.2.3 useContext 读取数据

子组件用 useContext 读取 Context 提供的数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useContext } from 'react'
import { ThemeContext, UserContext } from './contexts'

// 读取主题
function Header() {
  const { theme, toggleTheme } = useContext(ThemeContext)
  // 读取用户
  const user = useContext(UserContext)

  return (
    <header className={`header ${theme}`}>
      <img src={user.avatar} alt={user.name} />
      <button onClick={toggleTheme}>
        切换到{theme === 'light' ? '深色' : '浅色'}模式
      </button>
    </header>
  )
}

// 即使是深层嵌套的组件,也能直接拿到数据
function DeepButton() {
  const { theme, toggleTheme } = useContext(ThemeContext)

  return <button className={`btn ${theme}`}>{theme}</button>
}

11.3 Context 实战应用场景

11.3.1 主题切换:dark/light mode

主题切换是 Context 最经典的应用场景:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// theme/ThemeContext.js
import { createContext, useContext, useState } from 'react'

const ThemeContext = createContext()

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light')
  }

  const value = {
    theme,
    isDark: theme === 'dark',
    toggleTheme
  }

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  )
}

// 自定义 Hook:方便子组件读取
function useTheme() {
  const context = useContext(ThemeContext)
  if (!context) {
    throw new Error('useTheme 必须在 ThemeProvider 内使用')
  }
  return context
}

export { ThemeContext, ThemeProvider, useTheme }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// App.jsx
import { ThemeProvider } from './theme/ThemeContext'
import Header from './components/Header'
import Content from './components/Content'

function App() {
  return (
    <ThemeProvider>
      <div className="app">
        <Header />
        <Content />
      </div>
    </ThemeProvider>
  )
}

// Header.jsx
import { useTheme } from '../theme/ThemeContext'

function Header() {
  const { theme, isDark, toggleTheme } = useTheme()

  return (
    <header className={`header ${theme}`}>
      <h1>我的应用</h1>
      <button onClick={toggleTheme}>
        {isDark ? '🌙 深色' : '☀️ 浅色'}
      </button>
    </header>
  )
}

11.3.2 用户信息全局状态

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// auth/AuthContext.js
import { createContext, useContext, useState } from 'react'

const AuthContext = createContext(null)

function AuthProvider({ children }) {
  const [user, setUser] = useState(null)
  const [token, setToken] = useState(null)

  const login = (userData, authToken) => {
    setUser(userData)
    setToken(authToken)
    localStorage.setItem('token', authToken)
  }

  const logout = () => {
    setUser(null)
    setToken(null)
    localStorage.removeItem('token')
  }

  return (
    <AuthContext.Provider value={{ user, token, isAuthenticated: !!user, login, logout }}>
      {children}
    </AuthContext.Provider>
  )
}

function useAuth() {
  const context = useContext(AuthContext)
  if (!context) {
    throw new Error('useAuth 必须在 AuthProvider 内使用')
  }
  return context
}

export { AuthContext, AuthProvider, useAuth }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 任意组件都可以直接获取用户信息
function ProfileMenu() {
  const { user, isAuthenticated, logout } = useAuth()

  if (!isAuthenticated) {
    return <a href="/login">登录</a>
  }

  return (
    <div>
      <img src={user.avatar} alt={user.name} />
      <span>{user.name}</span>
      <button onClick={logout}>退出</button>
    </div>
  )
}

11.3.3 国际化语言切换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// i18n/I18nContext.js
import { createContext, useContext, useState } from 'react'

const translations = {
  zh: {
    greeting: '你好',
    welcome: '欢迎来到我的应用',
    logout: '退出'
  },
  en: {
    greeting: 'Hello',
    welcome: 'Welcome to my app',
    logout: 'Logout'
  }
}

const I18nContext = createContext()

function I18nProvider({ children }) {
  const [locale, setLocale] = useState('zh')

  const t = (key) => {
    return translations[locale][key] || key
  }

  return (
    <I18nContext.Provider value={{ locale, setLocale, t }}>
      {children}
    </I18nContext.Provider>
  )
}

function useI18n() {
  return useContext(I18nContext)
}

export { I18nProvider, useI18n }

11.3.4 全局配置参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// config/ConfigContext.js
import { createContext, useContext, useState } from 'react'

const ConfigContext = createContext()

function ConfigProvider({ children }) {
  const [config, setConfig] = useState({
    apiBaseUrl: 'https://api.example.com',
    uploadMaxSize: 10 * 1024 * 1024,  // 10MB
    enableAnalytics: true,
    maintenanceMode: false
  })

  const updateConfig = (updates) => {
    setConfig(prev => ({ ...prev, ...updates }))
  }

  return (
    <ConfigContext.Provider value={{ config, updateConfig }}>
      {children}
    </ConfigContext.Provider>
  )
}

function useConfig() {
  return useContext(ConfigContext)
}

export { ConfigProvider, useConfig }

11.4 Context 性能优化

先别急着优化! Context 的性能问题只有在特定场景下才会成为真正的瓶颈。在学习优化技巧之前,先问问自己:真的需要优化吗?用 React DevTools Profiler 确认问题再动手。

11.4.1 Context 每次更新导致所有消费者重渲染

Context 的一个重要性能问题是:当 Provider 的 value 变化时,所有消费这个 Context 的组件都会重新渲染

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function App() {
  const [theme, setTheme] = useState('light')
  const [user, setUser] = useState({ name: '小明' })

  // ❌ 问题:每次 App 重新渲染,这个对象都是新引用!
  // 导致所有消费 ThemeContext 的组件都重新渲染
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <UserContext.Provider value={{ user, setUser }}>
        <div>{/* 内容 */}</div>
      </UserContext.Provider>
    </ThemeContext.Provider>
  )
}

11.4.2 拆分 Context:按更新频率分离

解决方案一:把 Context 按更新频率拆分成多个。因为 Context 的 value 每次更新,所有消费者都会重新渲染,所以最好把频繁变化的值不频繁变化的值放在不同的 Context 里。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// ❌ 一个 Context 包含所有值,一个变化全都重渲染
const AppContext = createContext({
  theme: 'light',
  user: null,
  notifications: [],
  cartItems: []
})

// ✅ 拆成多个 Context,变化时只影响需要的组件
const ThemeContext = createContext()
const UserContext = createContext()
const NotificationContext = createContext()
const CartContext = createContext()

11.4.3 useMemo 优化 Context 值

useMemo 缓存 Context 的 value,避免不必要的引用变化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { useMemo } from 'react'

function App() {
  const [theme, setTheme] = useState('light')
  const [user, setUser] = useState({ name: '小明' })

  // 用 useMemo 缓存 value,只有当 theme 或 setTheme 变化时才重新创建
  const themeValue = useMemo(() => ({
    theme,
    setTheme
  }), [theme, setTheme])

  // user 没变化时,这个 value 不会重新创建
  const userValue = useMemo(() => ({
    user,
    setUser
  }), [user, setUser])

  return (
    <ThemeContext.Provider value={themeValue}>
      <UserContext.Provider value={userValue}>
        <div>{/* 内容 */}</div>
      </UserContext.Provider>
    </ThemeContext.Provider>
  )
}

11.4.4 memo + useCallback + Context 组合优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { memo, useCallback, useMemo, useState, useContext } from 'react'

// 子组件用 memo 包裹,只有 props 真正变化时才重新渲染
// 注意:这个子组件通过 props 接收回调,而不是直接从 Context 读
const ThemeToggle = memo(function ThemeToggle({ theme, onToggle }) {
  console.log('ThemeToggle 渲染了')  // 方便观察是否重渲染
  return <button onClick={onToggle}>{theme}</button>
})

// 父组件:用 useCallback 稳定回调函数
function Parent() {
  const [count, setCount] = useState(0)
  const [theme, setTheme] = useState('light')

  // 用 useCallback 保证函数引用稳定
  const handleToggle = useCallback(() => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light')
  }, [])

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      {/* 即使 count 变化导致 Parent 重渲染,ThemeToggle 也不会重渲染 */}
      {/* 因为 theme 没变,handleToggle 引用也没变 */}
      <ThemeToggle theme={theme} onToggle={handleToggle} />
    </div>
  )
}

⚠️ 这个例子的教训:如果子组件从 Context 读取数据(比如 useContext(ThemeContext)),那你用不用 memo + useCallback 都无所谓——Context value 变化了,子组件照样重渲染。上面例子的 ThemeToggle通过 props 接收 themeonToggle,这样 memo 才能真正发挥作用:只有当这两个 prop 真正变化时才重渲染。


本章小结

本章我们学习了 React 的"跨组件数据传递神器"——Context:

  • Props Drilling 问题:深层嵌套的组件需要数据时,数据要经过每一层中间组件,造成代码冗余和维护困难
  • Context 的解决方案:数据从 Provider 直接传递到消费者,无需经过中间层级
  • createContext + useContext:createContext 创建上下文,Provider 包裹子树并传递数据,子组件用 useContext 读取
  • 实战场景:主题切换(dark/light mode)、用户信息全局状态、国际化语言、全局配置参数
  • 性能优化:Context value 变化时所有消费者都会重新渲染,解决方法包括拆分 Context、useMemo 缓存 value、memo + useCallback 配合使用

Context 是 React 状态管理的重要工具,适合"真正需要全局共享"的数据。对于复杂应用的状态管理,还需要结合 useReducer、Zustand、Redux 等方案。下一章我们将学习 useReducer——复杂状态逻辑的克星!🧠