现在位置: 首页 > React 教程 > 正文

全局状态 — Context + useReducer

本章你将学会用 React Context + useReducer 管理跨组件、跨页面的共享数据,并实现「收藏文章」功能。


为什么需要全局状态?

到目前为止,数据通过 Props 层层传递。但有些数据需要被多个不相关的组件访问:

  • 收藏列表需要在首页、详情页、导航栏之间共享
  • 用户登录状态所有页面都要知道
  • 购物车数据需要跨页面访问

Props 逐层传递(Prop Drilling) 的痛点:A → B → C → D,中间组件被迫接收自己不用的 Props。

Context 提供了一个全局的「数据通道」,任何子孙组件都可以直接读取,跳过中间层。


Context 的核心概念

步骤代码作用
创建 ContextcreateContext()创建一个共享数据容器
提供数据<Provider value={...}>在组件树顶层包裹,注入数据
消费数据useContext(MyContext)在任意子孙组件中读取数据

createContext 与 Provider

实例

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

// 第一步:创建 Context
const ThemeContext = createContext(null)

// 第二步:创建 Provider 组件
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')
  const toggleTheme = () => setTheme(theme === 'light' ? 'dark' : 'light')

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

// 第三步:在任意组件中使用
function ThemedButton() {
  const { theme, toggleTheme } = useContext(ThemeContext)
  return (
    <button onClick={toggleTheme}>
      当前主题:{theme}
    </button>
  )
}

// 第四步:在 App 中包裹 Provider
function App() {
  return (
    <ThemeProvider>
      <ThemedButton />
    </ThemeProvider>
  )
}

Context 就像一个「广播塔」:Provider 往外广播数据,useContext 接收数据。


useReducer — 管理复杂状态

当状态逻辑变得复杂(多个子状态、多种更新方式),useState 就捉襟见肘了。

useReducer 提供了类似 Redux 的状态管理模式:dispatch action → reducer 计算新状态

实例

import { useReducer } from 'react'

// reducer 函数:(当前状态, action) ⇒ 新状态
function favoriteReducer(state, action) {
  switch (action.type) {
    case 'TOGGLE':
      // 如果已收藏则取消,未收藏则添加
      if (state.includes(action.id)) {
        return state.filter(id => id !== action.id)
      } else {
        return [...state, action.id]
      }
    case 'CLEAR':
      return []
    default:
      return state
  }
}

function FavoriteDemo() {
  // useReducer(reducer, 初始值)
  const [favoriteIds, dispatch] = useReducer(favoriteReducer, [])

  return (
    <div>
      <p>收藏数量:{favoriteIds.length}</p>
      <button onClick={() => dispatch({ type: 'TOGGLE', id: 1 })}>
        {favoriteIds.includes(1) ? '取消收藏 1' : '收藏 1'}
      </button>
      <button onClick={() => dispatch({ type: 'CLEAR' })}>清空收藏</button>
    </div>
  )
}

useState vs useReducer

场景推荐理由
单个独立状态useState简单直接
多个状态相互关联useReducer一次 dispatch 可以更新多个状态
更新逻辑复杂useReducerreducer 中有清晰的 action 类型,易于调试
需要传递给深层组件useState 或 useReducer都可以配合 Context 使用

Context + useReducer 组合模式

这是 React 中最强大的全局状态管理方案,也是 Redux 的核心思想。

实例

// 文件路径:src/context/FavoriteContext.jsx
import { createContext, useContext, useReducer, useEffect } from 'react'

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

// 2. 定义 reducer
function favoriteReducer(state, action) {
  switch (action.type) {
    case 'TOGGLE':
      if (state.includes(action.id)) {
        return state.filter(id => id !== action.id)
      } else {
        return [...state, action.id]
      }
    case 'CLEAR':
      return []
    default:
      return state
  }
}

// 3. Provider 组件:组合 Context + useReducer
export function FavoriteProvider({ children }) {
  // 从 localStorage 恢复初始状态
  const [favoriteIds, dispatch] = useReducer(favoriteReducer, [], () => {
    const saved = localStorage.getItem('blog-favorites')
    return saved ? JSON.parse(saved) : []
  })

  // 收藏列表变化时,自动同步到 localStorage
  useEffect(() => {
    localStorage.setItem('blog-favorites', JSON.stringify(favoriteIds))
  }, [favoriteIds])

  // 封装几个常用方法
  function toggleFavorite(id) {
    dispatch({ type: 'TOGGLE', id })
  }

  function isFavorite(id) {
    return favoriteIds.includes(id)
  }

  const value = {
    favoriteIds,
    favoriteCount: favoriteIds.length,
    toggleFavorite,
    isFavorite
  }

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

// 4. 自定义 Hook:封装 useContext,让调用方更简洁
export function useFavorites() {
  const context = useContext(FavoriteContext)
  if (!context) {
    throw new Error('useFavorites 必须在 FavoriteProvider 内部使用')
  }
  return context
}

在 main.jsx 中注册 Provider

实例

// 文件路径:src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import { FavoriteProvider } from './context/FavoriteContext'
import router from './router'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    {/* Provider 包裹在最外层,所有组件都能访问 */}
    <FavoriteProvider>
      <RouterProvider router={router} />
    </FavoriteProvider>
  </React.StrictMode>
)

为什么要把 Provider 放在 RouterProvider 外面?因为导航栏 NavBar(在 App 中)也需要读取收藏状态。Provider 必须包裹所有需要访问这个状态的组件。


在组件中使用收藏功能

BlogCard 中的收藏按钮

实例

// 文件路径:src/components/BlogCard.jsx
import { Link } from 'react-router-dom'
import { useFavorites } from '../context/FavoriteContext'

function BlogCard({ id, title, summary, date, category }) {
  const { isFavorite, toggleFavorite } = useFavorites()

  function handleFavorite(e) {
    e.preventDefault()   // 阻止 Link 跳转
    toggleFavorite(id)
  }

  return (
    <Link to={`/post/${id}`} className="card-link">
      <div className="card">
        <span className="tag">{category}</span>
        <h3>{title}</h3>
        <p>{summary}</p>
        <div className="card-footer">
          <span className="date">{date}</span>
          <button className="fav-btn" onClick={handleFavorite}>
            {isFavorite(id) ? '&#x2665;' : '♡'}
          </button>
        </div>
      </div>
    </Link>
  )
}

export default BlogCard

PostPage 中的收藏按钮

实例

// 文件路径:src/pages/PostPage.jsx(关键部分)
import { useFavorites } from '../context/FavoriteContext'

function PostPage() {
  // ... 其他代码
  const { isFavorite, toggleFavorite } = useFavorites()

  return (
    <article className="post-view">
      <div className="post-header">
        <h1>{article.title}</h1>
        <button className="fav-btn" onClick={() => toggleFavorite(article.id)}>
          {isFavorite(article.id) ? '&#x2665; 已收藏' : '♡ 收藏'}
        </button>
      </div>
      {/* ... */}
    </article>
  )
}

NavBar 中显示收藏数量

实例

// 文件路径:src/components/NavBar.jsx
import { useFavorites } from '../context/FavoriteContext'

function NavBar() {
  const { favoriteCount } = useFavorites()

  return (
    <header className="navbar">
      <a href="/" className="logo">RUNOOB Blog</a>
      <nav>
        <a href="/">首页</a>
        {favoriteCount > 0 && (
          <span className="fav-badge">收藏 {favoriteCount}</span>
        )}
      </nav>
    </header>
  )
}

首页收藏一篇文章后,进入详情页自动显示「已收藏」,导航栏的数字也同步更新——这就是 Context 全局共享的价值。


Context + useReducer vs Vue3 Pinia

特性React Context + useReducerVue3 Pinia
集成方式React 内置,无需安装独立库,需安装
State 定义useReducer 的初始值state: () => ({})
状态更新dispatch({ type, payload })直接修改或 action 函数
计算属性手动 useMemogetters
响应式方式Provider value 变化触发重新渲染自动依赖追踪
DevTools无内置(需 Redux DevTools)Vue DevTools 原生支持

本章小结

本章你掌握了 React 全局状态管理的完整方案:createContext 创建容器、Provider 注入数据、useContext 消费数据、useReducer 管理复杂更新逻辑。

「收藏文章」功能让首页、详情页、导航栏的数据保持全局同步,刷新页面后也能通过 localStorage 恢复。