Global State — Context + useReducer
In this chapter, you will learn how to use React Context + useReducer to manage shared data across components and pages, and implement a 'favorite articles' feature.
Why do we need global state?
So far, data is passed down layer by layer through Props. But some data needs to be accessed by multiple unrelated components:
- The favorite list needs to be shared between the home page, detail page, and navigation bar
- All pages need to know the user login status
- Cart data needs to be accessible across pages
Props passing layer by layer (Prop Drilling)The pain point: A → B → C → D, intermediate components are forced to receive Props they don't use.
ContextIt provides a global 'data channel' where any descendant component can read directly, skipping the intermediate layers.
Core Concepts of Context
| Steps | Code | Purpose |
|---|---|---|
| Create Context | createContext() | Create a shared data container |
| Provide Data | <Provider value={...}> | Wrap the top of the component tree and inject data |
| Consume Data | useContext(MyContext) | Read data in any descendant component |
createContext and Provider
Example
// Step 1: Create Context
const ThemeContext = createContext(null)
// Step 2: Create a Provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
const toggleTheme = () => setTheme(theme === 'light' ? 'dark' : 'light')
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
// Step 3: Use it in any component
function ThemedButton() {
const { theme, toggleTheme } = useContext(ThemeContext)
return (
<button onClick={toggleTheme}>
Current theme:{theme}
</button>
)
}
// Step 4: Wrap the Provider in App
function App() {
return (
<ThemeProvider>
<ThemedButton />
</ThemeProvider>
)
}
Context is like a 'broadcast tower': Provider broadcasts data outward, and useContext receives the data.
useReducer — Managing Complex State
When state logic becomes complex (multiple sub-states, multiple update methods), useState falls short.
useReducerIt provides a state management pattern similar to Redux:dispatch action → reducer computes the new state。
Example
// reducer function: (current state, action) ⇒ new state
function favoriteReducer(state, action) {
switch (action.type) {
case 'TOGGLE':
// Remove from favorites if already favorited, otherwise add
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, initial value)
const [favoriteIds, dispatch] = useReducer(favoriteReducer, [])
return (
<div>
<p>Favorite count:{favoriteIds.length}</p>
<button onClick={() => dispatch({ type: 'TOGGLE', id: 1 })}>
{favoriteIds.includes(1) ? 'Unfavorite 1' : 'Favorite 1'}
</button>
<button onClick={() => dispatch({ type: 'CLEAR' })}>Clear favorites</button>
</div>
)
}
useState vs useReducer
| Scenario | Recommendation | Reason |
|---|---|---|
| Single independent state | useState | Simple and direct |
| Multiple interrelated states | useReducer | One dispatch can update multiple states |
| Complex update logic | useReducer | The reducer has clear action types, making it easy to debug |
| Needs to be passed to deep components | useState or useReducer | Both can be used with Context |
Context + useReducer Combined Pattern
This is the most powerful global state management solution in React, and also the core idea of Redux.
Example
import { createContext, useContext, useReducer, useEffect } from 'react'
// 1. Create Context
const FavoriteContext = createContext(null)
// 2. Define the 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 component: combine Context + useReducer
export function FavoriteProvider({ children }) {
// Restore initial state from localStorage
const [favoriteIds, dispatch] = useReducer(favoriteReducer, [], () => {
const saved = localStorage.getItem('blog-favorites')
return saved ? JSON.parse(saved) : []
})
// Auto-sync to localStorage when the favorite list changes
useEffect(() => {
localStorage.setItem('blog-favorites', JSON.stringify(favoriteIds))
}, [favoriteIds])
// Wrap a few common methods
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. Custom Hook: wrap useContext to make the caller cleaner
export function useFavorites() {
const context = useContext(FavoriteContext)
if (!context) {
throw new Error('useFavorites must be used inside FavoriteProvider')
}
return context
}
Registering the Provider in main.jsx
Example
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>
{/* Wrap the Provider at the outermost layer so all components can access it */}
<FavoriteProvider>
<RouterProvider router={router} />
</FavoriteProvider>
</React.StrictMode>
)
Why put the Provider outside RouterProvider? Because the NavBar (in App) also needs to read the favorite state. The Provider must wrap all components that need access to this state.
Using the Favorite Feature in Components
The Favorite Button in BlogCard
Example
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() // Prevent Link navigation
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
The Favorite Button in PostPage
Example
import { useFavorites } from '../context/FavoriteContext'
function PostPage() {
// ... other code
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) ? '♥ Favorited' : '♡ Favorite'}
</button>
</div>
{/* ... */}
</article>
)
}
Displaying the Favorite Count in NavBar
Example
import { useFavorites } from '../context/FavoriteContext'
function NavBar() {
const { favoriteCount } = useFavorites()
return (
<header className="navbar">
<a href="/" className="logo">EXAMPLE Blog</a>
<nav>
<a href="/">Home</a>
{favoriteCount > 0 && (
<span className="fav-badge">Favorites{favoriteCount}</span>
)}
</nav>
</header>
)
}
After favoriting an article on the home page, the detail page automatically shows 'Favorited', and the number in the navigation bar updates in sync—this is the value of Context global sharing.
Context + useReducer vs Vue3 Pinia
| Feature | React Context + useReducer | Vue3 Pinia |
|---|---|---|
| Integration method | Built into React, no installation required | Standalone library, requires installation |
| State definition | Initial value of useReducer | state: () => ({}) |
| State update | dispatch({ type, payload }) | Direct modification or action functions |
| Computed properties | Manual useMemo | getters |
| Reactivity approach | Provider value changes trigger re-rendering | Automatic dependency tracking |
| DevTools | No built-in (requires Redux DevTools) | Vue DevTools native support |
Chapter Summary
In this chapter, you have mastered the complete solution for React global state management: createContext creates the container, Provider injects data, useContext consumes data, and useReducer manages complex update logic.
The 'favorite articles' feature keeps the data on the home page, detail page, and navigation bar globally synchronized, and it can be restored via localStorage after refreshing the page.
More Topics