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

StepsCodePurpose
Create ContextcreateContext()Create a shared data container
Provide Data<Provider value={...}>Wrap the top of the component tree and inject data
Consume DatauseContext(MyContext)Read data in any descendant component

createContext and Provider

Example

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

// 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

import { useReducer } from 'react'

// 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

ScenarioRecommendationReason
Single independent stateuseStateSimple and direct
Multiple interrelated statesuseReducerOne dispatch can update multiple states
Complex update logicuseReducerThe reducer has clear action types, making it easy to debug
Needs to be passed to deep componentsuseState or useReducerBoth 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

// File path: src/context/FavoriteContext.jsx
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

// File path: 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>
    {/* 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

// File path: 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()   // 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) ? '&#x2665;' : '♡'}
          </button>
        </div>
      </div>
    </Link>
  )
}

export default BlogCard

The Favorite Button in PostPage

Example

// File path: src/pages/PostPage.jsx (key part)
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) ? '&#x2665; Favorited' : '♡ Favorite'}
        </button>
      </div>
      {/* ... */}
    </article>
  )
}

Displaying the Favorite Count in NavBar

Example

// File path: src/components/NavBar.jsx
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

FeatureReact Context + useReducerVue3 Pinia
Integration methodBuilt into React, no installation requiredStandalone library, requires installation
State definitionInitial value of useReducerstate: () => ({})
State updatedispatch({ type, payload })Direct modification or action functions
Computed propertiesManual useMemogetters
Reactivity approachProvider value changes trigger re-renderingAutomatic dependency tracking
DevToolsNo 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