グローバル状態 — Context + useReducer
この章では、React Context + useReducer を使って、コンポーネント間・ページ間で共有データを管理し、「記事のお気に入り」機能を実装します。
なぜグローバル状態が必要なのか?
これまで、データは Props を通じて階層ごとに渡されてきました。しかし、互いに関連しない複数のコンポーネントからアクセスする必要があるデータもあります:
- お気に入りリストは、ホームページ、詳細ページ、ナビゲーションバー間で共有する必要があります
- ユーザーのログイン状態は、すべてのページが把握する必要があります
- カートのデータは、ページをまたいでアクセスする必要があります
Props の階層ごとの受け渡し(Prop Drilling)の問題点:A → B → C → D のように、途中のコンポーネントが自分では使わない Props を受け取らざるを得ません。
Contextグローバルな「データ通路」を提供し、任意の子孫コンポーネントが中間層をスキップして直接読み取ることができます。
Context の中核概念
| 手順 | コード | 役割 |
|---|---|---|
| Context を作成 | createContext() | 共有データコンテナを作成する |
| データを提供 | <Provider value={...}> | コンポーネントツリーの最上位でラップし、データを注入する |
| データを消費 | useContext(MyContext) | 任意の子孫コンポーネントでデータを読み取る |
createContext と Provider
実例
// ステップ 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 が新しい状態を計算する。
実例
// 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 | 1 回の dispatch で複数の状態を更新できる |
| 更新ロジックが複雑 | useReducer | reducer に明確な action タイプがあり、デバッグしやすい |
| 深い層のコンポーネントに渡す必要がある | useState または useReducer | どちらも Context と組み合わせて使用できる |
Context + useReducer 組み合わせパターン
これは React で最も強力なグローバル状態管理の方法であり、Redux の中核的な考え方でもあります。
実例
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 を登録する
実例
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 のお気に入りボタン
実例
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 のお気に入りボタン
実例
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 にお気に入り数を表示する
実例
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 + 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 から復元できます。
その他の拡張