全局状态 — Context + useReducer
本章你将学会用 React Context + useReducer 管理跨组件、跨页面的共享数据,并实现「收藏文章」功能。
为什么需要全局状态?
到目前为止,数据通过 Props 层层传递。但有些数据需要被多个不相关的组件访问:
- 收藏列表需要在首页、详情页、导航栏之间共享
- 用户登录状态所有页面都要知道
- 购物车数据需要跨页面访问
Props 逐层传递(Prop Drilling) 的痛点:A → B → C → D,中间组件被迫接收自己不用的 Props。
Context 提供了一个全局的「数据通道」,任何子孙组件都可以直接读取,跳过中间层。
Context 的核心概念
| 步骤 | 代码 | 作用 |
|---|---|---|
| 创建 Context | createContext() | 创建一个共享数据容器 |
| 提供数据 | <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
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>
)
}
// 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 可以更新多个状态 |
| 更新逻辑复杂 | useReducer | reducer 中有清晰的 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
}
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>
)
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) ? '♥' : '♡'}
</button>
</div>
</div>
</Link>
)
}
export default BlogCard
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) ? '♥' : '♡'}
</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) ? '♥ 已收藏' : '♡ 收藏'}
</button>
</div>
{/* ... */}
</article>
)
}
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) ? '♥ 已收藏' : '♡ 收藏'}
</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>
)
}
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 + useReducer | Vue3 Pinia |
|---|---|---|
| 集成方式 | React 内置,无需安装 | 独立库,需安装 |
| State 定义 | useReducer 的初始值 | state: () => ({}) |
| 状态更新 | dispatch({ type, payload }) | 直接修改或 action 函数 |
| 计算属性 | 手动 useMemo | getters |
| 响应式方式 | Provider value 变化触发重新渲染 | 自动依赖追踪 |
| DevTools | 无内置(需 Redux DevTools) | Vue DevTools 原生支持 |
本章小结
本章你掌握了 React 全局状态管理的完整方案:createContext 创建容器、Provider 注入数据、useContext 消费数据、useReducer 管理复杂更新逻辑。
「收藏文章」功能让首页、详情页、导航栏的数据保持全局同步,刷新页面后也能通过 localStorage 恢复。
