React useReducerとContext APIを組み合わせた本格的な状態管理ガイド
生徒
「Reactでアプリが大きくなってくると、データの管理がバラバラになって大変です。もっとまとめて管理する方法はありませんか?」
先生
「そんな時こそ、ユーズリデューサー(useReducer)とコンテキストエーピーアイ(Context API)の出番です。これらを組み合わせると、アプリ全体のデータを一つの司令塔で管理できるようになりますよ。」
生徒
「司令塔ですか!難しそうですが、初心者でも使いこなせますか?」
先生
「仕組みはレストランの注文に似ています。まずは基本的な考え方から順番に、丁寧に解説していきますね!」
1. useReducerとContext APIを組み合わせる理由
Reactで小さなアプリを作る時は、useState(ユーズステート)という機能でデータを管理すれば十分です。しかし、本格的なアプリになると、「どこでデータが変わったのかわからない」「あちこちの部品にデータを渡すのが面倒」といった問題が発生します。
そこで、データの変更ルールを専門に扱う「useReducer」と、データをアプリ全体に配信する「Context API」を組み合わせます。これにより、まるでテレビの放送局のように、一箇所で管理している最新のデータを、必要な部品だけがいつでも受信できるようになります。この組み合わせは、プロの開発現場でも非常に多く使われる、効率的な状態管理の王道パターンです。
2. リデューサー(reducer)の役割を例え話で理解する
まずは「useReducer」の仕組みを理解しましょう。リデューサーは、例えるなら「レストランの厨房(ちゅうぼう)」です。厨房には、お客さん(画面の部品)から「注文票(アクション)」が届きます。
注文票には「ハンバーグを一つ追加」や「サラダをキャンセル」といった具体的な指示が書かれています。料理人(リデューサー)は、現在の在庫(今のデータ)を確認し、注文票の通りに新しい料理(新しいデータ)を作ります。このように、データの変え方を一箇所にまとめることで、勝手にデータが書き換わるミスを防ぐことができます。プログラミングにおいて、この注文を出す行為を「ディスパッチ(dispatch)」と呼びます。
3. ステップ1:リデューサー関数を作ってみよう
まずは、データの変更ルールを書いたリデューサー関数を作成します。ここでは、簡単なカウンターアプリを例にします。数字を増やす、減らす、ゼロに戻すといったルールを記述します。
// 現在のデータ(state)と、指示書(action)を受け取ります
function counterReducer(state, action) {
switch (action.type) {
case "INCREMENT":
return { count: state.count + 1 };
case "DECREMENT":
return { count: state.count - 1 };
case "RESET":
return { count: 0 };
default:
return state;
}
}
4. ステップ2:コンテキスト(データの放送局)を準備する
次に、作ったデータを配るための「コンテキスト」を作成します。Reactの createContext(クリエイトコンテキスト)を使います。これを使うことで、深い階層にある部品まで、バケツリレーをせずにデータを届けることができます。
import React, { createContext, useReducer } from "react";
// データを配るための箱を二つ作ります(値用と、命令用)
export const StateContext = createContext();
export const DispatchContext = createContext();
export function CounterProvider({ children }) {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<StateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>
{children}
</DispatchContext.Provider>
</StateContext.Provider>
);
}
5. ステップ3:部品からデータを呼び出す
準備ができたら、実際に画面に数字を表示する部品を作ります。ここで useContext(ユーズコンテキスト)という機能を使います。これを使えば、放送局から流れている最新のデータをキャッチできます。
import React, { useContext } from "react";
import { StateContext } from "./App";
function CounterDisplay() {
// 放送局から現在のデータ(state)を受け取ります
const state = useContext(StateContext);
return (
<div className="text-center p-3">
<h2 className="display-4">現在の数値:{state.count}</h2>
</div>
);
}
6. ステップ4:注文(ディスパッチ)を出してデータを更新する
最後に、データを変更するためのボタンを作ります。ここでは、先ほど準備した dispatch(ディスパッチ)という機能を使います。これに「INCREMENT(増やして!)」という指示を載せて送ることで、リデューサーが動き出します。
import React, { useContext } from "react";
import { DispatchContext } from "./App";
function CounterButtons() {
// 命令を送るための dispatch を受け取ります
const dispatch = useContext(DispatchContext);
return (
<div className="d-flex justify-content-center gap-2">
<button
className="btn btn-primary"
onClick={() => dispatch({ type: "INCREMENT" })}
>
増やす
</button>
<button
className="btn btn-danger"
onClick={() => dispatch({ type: "DECREMENT" })}
>
減らす
</button>
</div>
);
}
7. この組み合わせを使うメリット(パフォーマンスと保守性)
なぜこのように面倒な手順を踏むのでしょうか。それは、アプリが成長した時に「誰がいつデータを変えたのか」をはっきりさせるためです。全ての変更がリデューサーを通るため、デバッグ(不具合探し)が非常に楽になります。
また、Context APIを二つ(データ用と命令用)に分けることで、無駄な再描画を防ぐことができます。数字が変わった時に、数字を表示している部品だけが動き、ボタン部品は動かないように設定できるのです。パソコンを触ったことがない方でも、「役割分担をしっかりすることで、無駄な電気を使わずにサクサク動く」とイメージしていただければ正解です。
8. 実践的な活用シーン:買い物かごやログイン管理
この仕組みは、ショッピングサイトの「買い物かご」や、ユーザーがログインしているかどうかの「ログイン状態管理」で特によく使われます。買い物かごは、商品ページ、カート画面、ヘッダーの合計金額など、たくさんの場所で同じデータを使いますよね。
今回学んだ「useReducer + Context API」を使えば、商品をカゴに入れるという一箇所のアクションだけで、サイト中の全ての金額表示を正しく更新できます。一見難しく感じるコードも、一つ一つの部品の役割(料理人、注文票、放送局、受信機)を整理すれば、必ず理解できるようになります。少しずつ自分でもプログラムを書いて、動く楽しさを体験してみてください。