グローバル状態 — 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'

// ステップ 1:Context を作成
const ThemeContext = createContext(null)

// ステップ 2: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>
  )
}

// ステップ 3:任意のコンポーネントで使用
function ThemedButton() {
  const { theme, toggleTheme } = useContext(ThemeContext)
  return (
    <button onClick={toggleTheme}>
現在のテーマ:{theme}
    </button>
  )
}

// ステップ 4:App で Provider をラップ
function App() {
  return (
    <ThemeProvider>
      <ThemedButton />
    </ThemeProvider>
  )
}

Context は「放送塔」のようなものです:Provider がデータを外部に放送し、useContext がデータを受け取ります。


useReducer — 複雑な状態の管理

状態ロジックが複雑になると(複数のサブ状態、複数の更新方法)、useState では手が回らなくなります。

useReducerRedux に似た状態管理パターンを提供します: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シンプルで直接的
複数の状態が相互に関連useReducer1 回の 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">サンプル 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 から復元できます。

その他の拡張