React: Context 与全局状态

最后更新:2026-08-26

1. 你将学到



2. 一个主题切换的故事

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

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

▶ 示例:Context使用

JSX
// 需求:用户切换黑暗模式,所有子组件都跟着变

// 第 5 层:最里层的 Button 需要知道 theme
function Button({ theme }) {
  return <button style={{
    backgroundColor: theme === 'dark' ? '#333' : '#f0f0f0',
    color: theme === 'dark' ? 'white' : '#333'
  }}>按钮</button>
}

// 第 4 层
function Sidebar({ theme }) {
  return <div><Button theme={theme} /></div>
}

// 第 3 层
function MainLayout({ theme }) {
  return <div><Sidebar theme={theme} /></div>
}

// 第 2 层
function Page({ theme }) {
  return <MainLayout theme={theme} />
}

// 第 1 层:App 有 theme 状态
function App() {
  const [theme, setTheme] = useState('light')
  return <Page theme={theme} />  // ❌ theme 穿透 5 层
}
▶ 试一试

Alice 的问题:

(2) Context 的解法

JSX
// 1. 创建 Context
const ThemeContext = React.createContext('light')

// 2. Provider 提供数据
function App() {
  const [theme, setTheme] = useState('light')
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Page />
    </ThemeContext.Provider>
  )
}

// 中间组件完全不关心 theme
function Page() { return <MainLayout /> }
function MainLayout() { return <Sidebar /> }
function Sidebar() { return <Button /> }

// 5. 只有需要的组件才读取 Context
function Button() {
  const { theme, setTheme } = useContext(ThemeContext)
  return (
    <button style={{
      backgroundColor: theme === 'dark' ? '#333' : '#f0f0f0',
      color: theme === 'dark' ? 'white' : '#333'
    }}
    onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}>
      {theme === 'dark' ? '🌙' : '☀️'} 切换主题
    </button>
  )
}
▶ 试一试

收益:中间组件(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
JSX
// 1. 创建 Context(可以给默认值)
const UserContext = createContext({ name: '访客', role: 'guest' })
const ThemeContext = createContext('light')

// 2. Provider 提供数据(包裹需要访问的组件树)
function App() {
  const [user, setUser] = useState({ name: 'Alice', role: 'admin' })
  return (
    <UserContext.Provider value={{ user, setUser }}>
      <ThemeContext.Provider value="dark">
        <Dashboard />
      </ThemeContext.Provider>
    </UserContext.Provider>
  )
}

// 3. 任何子组件读取 Context
function Dashboard() {
  const { user } = useContext(UserContext)
  const theme = useContext(ThemeContext)
  return <p>{user.name} - 主题:{theme}</p>
}
▶ 试一试

(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

JSX
// ============================================
// 示例:自定义 ThemeProvider + useTheme Hook
// ============================================

const ThemeContext = React.createContext(null)

function ThemeProvider({ children }) {
  const [theme, setTheme] = React.useState(() => {
    // 从 localStorage 读取初始主题
    return localStorage.getItem('theme') || 'light'
  })

  function toggleTheme() {
    setTheme(prev => {
      const next = prev === 'light' ? 'dark' : 'light'
      localStorage.setItem('theme', next)  // 持久化
      return next
    })
  }

  const value = React.useMemo(
    () => ({ theme, toggleTheme }),
    [theme]
  )

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

// 自定义 Hook:封装 useContext + 错误检查
function useTheme() {
  const context = useContext(ThemeContext)
  if (!context) {
    throw new Error('useTheme 必须在 ThemeProvider 内部使用')
  }
  return context
}

// 使用
function ThemedButton() {
  const { theme, toggleTheme } = useTheme()
  return (
    <button onClick={toggleTheme}
      style={{
        backgroundColor: theme === 'dark' ? '#333' : '#f0f0f0',
        color: theme === 'dark' ? 'white' : '#333',
        padding: '10px 20px',
        border: 'none',
        borderRadius: '4px',
        cursor: 'pointer'
      }}>
      当前主题:{theme === 'dark' ? '🌙' : '☀️'} 点击切换
    </button>
  )
}


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 的"复杂状态管理",可以构建一个轻量级全局状态方案:

JSX
// ============================================
// 示例:全局通知系统(Context + useReducer)
// ============================================

// ---- 1. 创建 Context ----
const NotificationContext = createContext(null)

// ---- 2. 定义 reducer ----
function notificationReducer(state, action) {
  switch (action.type) {
    case 'ADD':
      return [...state, {
        id: Date.now(),
        message: action.message,
        type: action.notifType || 'info',
        read: false
      }]
    case 'MARK_READ':
      return state.map(n =>
        n.id === action.id ? { ...n, read: true } : n
      )
    case 'REMOVE':
      return state.filter(n => n.id !== action.id)
    case 'CLEAR_ALL':
      return []
    default:
      return state
  }
}

// ---- 3. Provider 组件 ----
function NotificationProvider({ children }) {
  const [notifications, dispatch] = useReducer(notificationReducer, [])

  // 用 useMemo 稳定 value
  const value = useMemo(() => ({ notifications, dispatch }), [notifications])

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

// ---- 4. 自定义 Hook ----
function useNotification() {
  const context = useContext(NotificationContext)
  if (!context) throw new Error('useNotification 必须在 NotificationProvider 内使用')
  return context
}

// ---- 5. 用通知的组件 ----
function NotificationBell() {
  const { notifications, dispatch } = useNotification()
  const unread = notifications.filter(n => !n.read).length

  return (
    <div style={{ position: 'relative', cursor: 'pointer' }}>
      🔔
      {unread > 0 && (
        <span style={{
          position: 'absolute', top: '-8px', right: '-8px',
          backgroundColor: '#ff4d4f', color: 'white',
          borderRadius: '50%', width: '18px', height: '18px',
          fontSize: '12px', display: 'flex', alignItems: 'center',
          justifyContent: 'center'
        }}>
          {unread}
        </span>
      )}
    </div>
  )
}

function NotificationList() {
  const { notifications, dispatch } = useNotification()

  if (notifications.length === 0) {
    return <p style={{ color: '#999', textAlign: 'center' }}>暂无通知</p>
  }

  return (
    <div style={{ maxWidth: '400px' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
        <strong>通知({notifications.length})</strong>
        <button onClick={() => dispatch({ type: 'CLEAR_ALL' })} style={smallBtn}>全部清除</button>
      </div>
      {notifications.map(n => (
        <div key={n.id} style={{
          padding: '8px', marginBottom: '4px',
          backgroundColor: n.read ? '#fafafa' : '#e6f7ff',
          borderRadius: '4px', cursor: 'pointer',
          display: 'flex', justifyContent: 'space-between',
          alignItems: 'center'
        }}
        onClick={() => dispatch({ type: 'MARK_READ', id: n.id })}>
          <span>
            {n.type === 'error' ? '❌' : n.type === 'success' ? '✅' : 'ℹ️'}
            {n.message}
          </span>
          <button onClick={e => { e.stopPropagation(); dispatch({ type: 'REMOVE', id: n.id }) }}
            style={{ ...smallBtn, color: '#ff4d4f' }}>✕</button>
        </div>
      ))}
    </div>
  )
}

// ---- 发通知的组件 ----
function NotificationSender() {
  const { dispatch } = useNotification()
  const [message, setMessage] = useState('')

  function send(type) {
    if (!message.trim()) return
    dispatch({ type: 'ADD', message: message.trim(), notifType: type })
    setMessage('')
  }

  return (
    <div>
      <input value={message} onChange={e => setMessage(e.target.value)}
        placeholder="通知内容" style={{ width: '100%', padding: '8px', marginBottom: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }} />
      <div style={{ display: 'flex', gap: '4px' }}>
        <button onClick={() => send('info')} style={typeBtn('#1890ff')}>ℹ️ info</button>
        <button onClick={() => send('success')} style={typeBtn('#52c41a')}>✅ 成功</button>
        <button onClick={() => send('error')} style={typeBtn('#ff4d4f')}>❌ 错误</button>
      </div>
    </div>
  )
}

const smallBtn = { padding: '2px 8px', fontSize: '12px', border: 'none', backgroundColor: 'transparent', cursor: 'pointer' }
const typeBtn = (color) => ({ padding: '6px 12px', backgroundColor: color, color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' })

// ---- 6. 组装 ----
function NotificationApp() {
  return (
    <NotificationProvider>
      <div style={{ maxWidth: '500px', margin: '0 auto' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
          <h2>🔔 通知系统</h2>
          <NotificationBell />
        </div>
        <NotificationSender />
        <hr style={{ margin: '16px 0' }} />
        <h3>通知列表</h3>
        <NotificationList />
      </div>
    </NotificationProvider>
  )
}

预期输出:完整通知系统。顶部有通知铃铛(显示未读数),中间可以发 info/success/error 三种通知,下方显示通知列表,每条通知可标记已读或删除。


▶ 示例 2:用户认证状态管理(Context + 自定义 Hook)

JSX 📖 仅展示
// ============================================
// 示例:用户认证系统——Context + 自定义 Hook 的最佳实践
// 功能:管理登录/登出状态,全局提供用户信息
// ============================================

import { createContext, useContext, useState, useCallback, useMemo } from 'react'

// ---- 1. 创建 Context ----
const AuthContext = createContext(null)

// ---- 2. Provider 组件 ----
function AuthProvider({ children }) {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(false)

  const login = useCallback(async (username, password) => {
    setLoading(true)
    try {
      // 模拟登录请求
      await new Promise(resolve => setTimeout(resolve, 800))
      if (username && password) {
        const loggedInUser = {
          id: 1,
          name: username,
          email: `${username}@example.com`,
          avatar: 'https://i.pravatar.cc/40',
          role: username === 'admin' ? '管理员' : '普通用户'
        }
        setUser(loggedInUser)
        localStorage.setItem('user', JSON.stringify(loggedInUser))
        return { success: true }
      }
      return { success: false, error: '用户名或密码错误' }
    } finally {
      setLoading(false)
    }
  }, [])

  const logout = useCallback(() => {
    setUser(null)
    localStorage.removeItem('user')
  }, [])

  // 用 useMemo 稳定 value 引用,避免 Provider 下所有子组件重渲染
  const value = useMemo(() => ({
    user,
    loading,
    isAuthenticated: !!user,
    login,
    logout
  }), [user, loading, login, logout])

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

// ---- 3. 自定义 Hook + 错误检查 ----
function useAuth() {
  const context = useContext(AuthContext)
  if (!context) {
    throw new Error('useAuth 必须在 AuthProvider 内部使用')
  }
  return context
}

// ---- 4. 使用认证的组件 ----
function LoginForm() {
  const { login, loading } = useAuth()
  const [username, setUsername] = useState('')
  const [password, setPassword] = useState('')
  const [error, setError] = useState('')

  async function handleSubmit(e) {
    e.preventDefault()
    setError('')
    const result = await login(username, password)
    if (!result.success) {
      setError(result.error)
    }
  }

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: '400px', margin: '0 auto' }}>
      <h2>🔐 登录</h2>
      {error && <p style={{ color: '#ff4d4f', fontSize: '14px' }}>{error}</p>}
      <input value={username} onChange={e => setUsername(e.target.value)}
        placeholder="用户名(输入 admin 体验管理员角色)"
        style={{ width: '100%', padding: '8px', marginBottom: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }} />
      <input type="password" value={password} onChange={e => setPassword(e.target.value)}
        placeholder="密码"
        style={{ width: '100%', padding: '8px', marginBottom: '8px', border: '1px solid #d9d9d9', borderRadius: '4px' }} />
      <button type="submit" disabled={loading}
        style={{ width: '100%', padding: '10px', backgroundColor: '#1890ff', color: 'white', border: 'none', borderRadius: '4px', cursor: loading ? 'not-allowed' : 'pointer' }}>
        {loading ? '登录中...' : '登录'}
      </button>
    </form>
  )
}

function UserDashboard() {
  const { user, logout } = useAuth()

  return (
    <div style={{ maxWidth: '400px', margin: '0 auto' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '16px' }}>
        <img src={user.avatar} alt={user.name}
          style={{ width: '40px', height: '40px', borderRadius: '50%' }} />
        <div>
          <h3 style={{ margin: 0 }}>{user.name}</h3>
          <p style={{ margin: '4px 0 0 0', color: '#666', fontSize: '13px' }}>
            {user.email} · {user.role}
          </p>
        </div>
      </div>
      <button onClick={logout}
        style={{ padding: '8px 16px', backgroundColor: '#ff4d4f', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
        退出登录
      </button>
    </div>
  )
}

// ---- 5. 顶层组件 ----
function AuthApp() {
  const { isAuthenticated } = useAuth()
  return isAuthenticated ? <UserDashboard /> : <LoginForm />
}

// ---- 6. 入口 ----
function App() {
  return (
    <AuthProvider>
      <div style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
        <AuthApp />
      </div>
    </AuthProvider>
  )
}
逻辑代码 114 行(超过 40 行限制,仅展示)

▶ 示例 3:多层 Provider 组合与 i18n 国际化

JSX 📖 仅展示
const I18nContext = createContext(null)

function I18nProvider({ children }) {
  const [locale, setLocale] = useState('zh')
  const translations = {
    zh: { title: '应用标题', greeting: '你好', logout: '退出' },
    en: { title: 'App Title', greeting: 'Hello', logout: 'Logout' },
  }
  const t = useCallback((key) => translations[locale][key] || key, [locale])
  const value = useMemo(() => ({ locale, setLocale, t }), [locale, t])
  return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>
}

function useI18n() {
  const ctx = useContext(I18nContext)
  if (!ctx) throw new Error('useI18n must be inside I18nProvider')
  return ctx
}

function LocaleSwitcher() {
  const { locale, setLocale } = useI18n()
  return (
    <select value={locale} onChange={e => setLocale(e.target.value)}
      style={{ padding: '4px 8px', borderRadius: 4 }}>
      <option value="zh">中文</option>
      <option value="en">English</option>
    </select>
  )
}

function Greeting() {
  const { t } = useI18n()
  return <h2>{t('greeting')}</h2>
}

function I18nApp() {
  return (
    <I18nProvider>
      <div style={{ maxWidth: 400, margin: '0 auto' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between' }}>
          <Greeting />
          <LocaleSwitcher />
        </div>
      </div>
    </I18nProvider>
  )
}
逻辑代码 42 行(超过 40 行限制,仅展示)

▶ 示例 4:拆分 Context 优化性能

JSX 📖 仅展示
const UserStateContext = createContext(null)
const UserDispatchContext = createContext(null)

function UserProvider({ children }) {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(false)

  const dispatch = useMemo(() => ({
    login: async (name) => {
      setLoading(true)
      await new Promise(r => setTimeout(r, 500))
      setUser({ name, role: name === 'admin' ? 'Admin' : 'User' })
      setLoading(false)
    },
    logout: () => setUser(null),
  }), [])

  const stateValue = useMemo(() => ({ user, loading }), [user, loading])

  return (
    <UserDispatchContext.Provider value={dispatch}>
      <UserStateContext.Provider value={stateValue}>
        {children}
      </UserStateContext.Provider>
    </UserDispatchContext.Provider>
  )
}

function useUserState() { return useContext(UserStateContext) }
function useUserDispatch() { return useContext(UserDispatchContext) }

function LoginButton() {
  const { login } = useUserDispatch()
  const { loading } = useUserState()
  return <button disabled={loading} onClick={() => login('Alice')}>Login</button>
}

function UserInfo() {
  const { user } = useUserState()
  if (!user) return <p>Not logged in</p>
  return <p>Logged in as {user.name} ({user.role})</p>
}

function SplitContextApp() {
  return (
    <UserProvider>
      <div style={{ maxWidth: 400, margin: '0 auto' }}>
        <UserInfo />
        <LoginButton />
      </div>
    </UserProvider>
  )
}
逻辑代码 45 行(超过 40 行限制,仅展示)

▶ 示例 5:useContext 替代 Props Drilling 完整示例

JSX 📖 仅展示
const SettingsContext = createContext(null)

function SettingsProvider({ children }) {
  const [settings, setSettings] = useState({
    theme: 'light', fontSize: 14, sidebarCollapsed: false,
  })

  const updateSetting = useCallback((key, value) => {
    setSettings(prev => ({ ...prev, [key]: value }))
  }, [])

  const value = useMemo(() => ({ settings, updateSetting }), [settings, updateSetting])

  return <SettingsContext.Provider value={value}>{children}</SettingsContext.Provider>
}

function useSettings() {
  const ctx = useContext(SettingsContext)
  if (!ctx) throw new Error('useSettings must be inside SettingsProvider')
  return ctx
}

function Toolbar() {
  const { settings, updateSetting } = useSettings()
  return (
    <div style={{ padding: 8, background: settings.theme === 'dark' ? '#333' : '#f0f0f0', display: 'flex', gap: 8, alignItems: 'center' }}>
      <button onClick={() => updateSetting('theme', settings.theme === 'dark' ? 'light' : 'dark')}>
        {settings.theme === 'dark' ? '☀️' : '🌙'}
      </button>
      <button onClick={() => updateSetting('sidebarCollapsed', !settings.sidebarCollapsed)}>
        {settings.sidebarCollapsed ? '▶' : '◀'} Sidebar
      </button>
      <select value={settings.fontSize} onChange={e => updateSetting('fontSize', Number(e.target.value))}>
        <option value={12}>12px</option>
        <option value={14}>14px</option>
        <option value={16}>16px</option>
        <option value={18}>18px</option>
      </select>
    </div>
  )
}

function Content() {
  const { settings } = useSettings()
  return (
    <div style={{ fontSize: settings.fontSize, color: settings.theme === 'dark' ? '#eee' : '#333',
      background: settings.theme === 'dark' ? '#1a1a1a' : '#fff', padding: 16 }}>
      <p>Current settings:</p>
      <ul>
        <li>Theme: {settings.theme}</li>
        <li>Font size: {settings.fontSize}px</li>
        <li>Sidebar: {settings.sidebarCollapsed ? 'collapsed' : 'expanded'}</li>
      </ul>
    </div>
  )
}

function SettingsApp() {
  return (
    <SettingsProvider>
      <div style={{ maxWidth: 600, margin: '0 auto' }}>
        <Toolbar />
        <Content />
      </div>
    </SettingsProvider>
  )
}
逻辑代码 59 行(超过 40 行限制,仅展示)

❓ 常见问题

Q Context 的 value 每次都是新对象,导致所有 Consumer 都重渲染怎么办?
AuseMemo 稳定 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,提供 userlogin(username, password)logout() 方法,管理用户认证状态。
  3. 挑战题(难度⭐⭐⭐):创建一个 CartProvider + useCart Hook,使用 Context + useReducer 实现购物车,包含添加、删除、修改数量、清空功能,用 useMemo 优化避免不必要重渲染。
Web-Tutorial.com

Web-Tutorial 技术团队

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

100%

🙏 帮我们做得更好

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

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