こんにちは!Reactの世界へようこそ。チーフアーキテクトの私です。
Reactを触り始めて最初に覚える魔法のフック、それが `useState` ですよね。「画面のデータを保持して、変わったら再描画してくれるなんて便利!」と、最初は誰もが感動します。
でも、アプリが少しずつ育ってきて、ボタンを押すたびに「あれ?画面の値がなんかズレる……」「あっちを直したらこっちがバグった……」なんて壁にぶつかったことはありませんか?
大丈夫ですよ、それはあなたが下手なわけでも、Reactが意地悪なわけでもありません。みんなが通る「最初の試練」です。
今日は、そのモヤモヤをスッキリ解消するために、「useStateの限界と、救世主であるuseReducerへのバトンタッチのタイミング」について、身近な例えを交えながらお話ししていきますね。
—
1. 脳内イメージ:お出かけ前の「カバン」で例えてみる
まずは、`useState` と `useReducer` の違いを、私たちの身近な「カバン」でイメージしてみましょう。
📌 useState は「小さなポシェット」
お財布とスマホだけを入れて、ちょっと近所のコンビニに行くときのポシェットです。
「お財布を取り出す」「スマホを入れる」といった単純な動作なら、ポシェットからサッと取り出せてすごく楽ちん。これが `useState` の世界です。一つの変数に対して「今の値はこれ!」とシンプルに管理できます。
📌 useReducer は「仕切りがたくさんある多機能な旅行用スーツケース」
家族みんなの着替え、お土産スペース、洗面用具、薬など、色々なものがごちゃ混ぜにならないように専用のポケットがいくつもあります。
さらに、荷物を詰めるときも「お父さんの服はここに、お母さんの服はこっちに、ルールに従って綺麗に収納する」という手順書(ルールブック)がしっかり決まっています。
Reactのコードでも同じです。
「ただの数字を1つ増やすだけ」「入力フォームの文字を1つ覚えるだけ」ならポシェット(`useState`)で十分ですが、「複数のデータが複雑に絡み合い、どういう順番でどう変えるべきかルールが必要」になってきたら、スーツケース(`useReducer`)の出番なんです。
—
2. useStateが「限界を迎える瞬間」とは?
初心者の方がよくやってしまうのが、次のような「なんでもかんでも `useState`」のパターンです。
// ユーザー登録フォームを全部1つのstateで作ろうとした悲劇の例
const [name, setName] = useState(”);
const [email, setEmail] = useState(”);
const [age, setAge] = useState(0);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState(null);
これくらいならまだ可愛いものですが、実際の現場では、「Aを変えたら、BとCのバリデーションも走らせて、もしエラーがなければDをリセットしつつ…」といった、連鎖する複雑な状態更新が生まれます。
そうすると何が起きるか?
- どこで値を更新しているのか、コードが散らばって追えなくなる
- 非同期のタイミングで古い値(Stale State)を掴んでしまい、意図しない画面バグが起きる
- 「あれ?この値とこの値、同時に変わらなきゃいけないのに、片方だけ更新されちゃった!」という不整合が起きる
もし、あなたの書いたコンポーネントの中で `set` 関数が何個も乱れ飛んでいたら……そろそろお引越しのサインです。
—
3. useReducer へバトンタッチする判断基準
「じゃあ、いつ `useReducer` に切り替えたらいいの?」という明確な基準を3つお伝えします。このどれかに当てはまったら、迷わず `useReducer` を使いましょう!
1. 状態(State)が3つ以上の関連する値で構成されているとき(例:フォームの入力値・エラー・ローディング中フラグが一体になっている)
2. 次の状態が「前の状態」に深く依存しており、更新のルール(シナリオ)が複数あるとき
3. 「何をしたいか(アクション)」と「どう状態を変えるか(ロジック)」をきれいに分離して、テストしやすくしたいとき
`useReducer` は、状態の変更手続きを 「Reducer(reducer関数)」 というひとつの場所に整理整頓します。「こういうことが起きたら(Action)、状態をこう変える(State更新)」というルールブックを外側に切り出すイメージです。これにより、コンポーネントの中身が驚くほどスッキリします。
—
4. 実例で見る!お買い物カートで学ぶ移行のビフォーアフター
百聞は一見にしかず。みんな大好き「お買い物カート」を例に、`useState` から `useReducer` への進化を見てみましょう。
パターンA:useState でがんばる限界の姿
まずは、`useState` で頑張って商品を管理しようとしたコードです。
import React, { useState } from ‘react’;
function ShoppingCartWithState() {
const [items, setItems] = useState([]);
const [totalPrice, setTotalPrice] = useState(0);
const [discountCode, setDiscountCode] = useState(”);
const [isDiscountApplied, setIsDiscountApplied] = useState(false);
// 商品を追加する関数(金額の計算や割引の判定が混ざってごちゃごちゃしやすい…)
const addItem = (item) => {
setItems([…items, item]);
// 毎回合計金額の計算も手動でやらないといけない…
const newTotal = totalPrice + item.price;
setTotalPrice(newTotal);
// もし割引コードが適用されていたら、ここでの計算も修正しなきゃ…?うわ、面倒くさい!
if (isDiscountApplied) {
// 複雑な再計算ロジックがここにダラダラと続く…
}
};
return (
カートの商品数: {items.length}件
合計: {totalPrice}円
{/ ボタンのクリックイベントなど /}
);
}
うーん、これだと「商品を増やす」という単純な操作の裏で、合計金額や割引の計算があちこちに散らばってしまい、バグの温床になってしまいますね。
—
パターンB:useReducer で美しく整頓された世界
これを `useReducer` で書き換えてみましょう。ルールブック(reducer)を外側に作るのがポイントです。
import React, { useReducer } from ‘react’;
// 1. 最初のお財布の状態(初期値)を定義します
const initialState = {
items: [],
totalPrice: 0,
discountApplied: false,
};
// 2. 「どんな命令(Action)が来たら、どう状態を変えるか」のルールブックを作る
// ※ここが useReducer の心臓部です!
function cartReducer(state, action) {
switch (action.type) {
case ‘ADD_ITEM’: {
const updatedItems = […state.items, action.payload];
const newTotal = state.totalPrice + action.payload.price;
// 割引がすでに適用されている場合の計算も、ここに一元化できる!
const finalTotal = state.discountApplied ? newTotal 0.9 : newTotal;
return {
…state,
items: updatedItems,
totalPrice: finalTotal,
};
}
case ‘APPLY_DISCOUNT’: {
// 割引コード適用のアクション
if (state.discountApplied) return state; // すでに適用済なら何もしない
return {
…state,
discountApplied: true,
totalPrice: state.totalPrice 0.9, // 10%オフにする
};
}
default:
// 想定外のアクションが来たときのエラーハンドリング
throw new Error(`未知のアクションタイプです: ${action.type}`);
}
}
function ShoppingCartWithReducer() {
// 3. コンポーネントの中では、reducerと初期値をセットするだけ!
// state:現在のデータ, dispatch:ルールブックに命令を送るための専用ポスト
const [state, dispatch] = useReducer(cartReducer, initialState);
return (
カートの商品数: {state.items.length}件
合計: {state.totalPrice}円
{/ 4. 「これをしたい!」という意図(アクション)だけを dispatch に投げる /}
);
}
export default ShoppingCartWithReducer;
どうでしょう?コンポーネントの中(`ShoppingCartWithReducer`)が非常にスッキリしましたよね。
「どうやって計算するか」という面倒なロジックはすべて上の `cartReducer` 関数に閉じ込められています。コンポーネント側は、「コーヒー豆を追加して!」(`dispatch`)とお願いするだけでよくなりました。
これが、実務で `useReducer` が愛される理由です。コードの「見通し」が劇的に良くなるんです。
—
5. チーフアーキテクトからの温かいアドバイス
最初は `useReducer` を見たとき、「なんだかコードの行数が増えるし、ボイラープレート(お決まりの記述)が多くて難しそう……」と感じるかもしれません。
でも、安心してください。
最初から無理に使う必要はまったくありません。小さな機能や、独立した1つの値を変えるだけなら、いままで通り `useState` で全然OKです。
ただ、
- 「あ、この画面、管理するデータが増えてきてあちこちで書き換えてるな……」
- 「状態の更新ルールが複雑になって、自分でもどこでバグってるか分からなくなってきたな……」
そう感じた瞬間が、ポシェットからスーツケースへお引越しするベストタイミングです。そのときは、ぜひこの記事を思い出して `useReducer` に挑戦してみてください。あなたのReactコードが、見違えるほどエレガントで頑丈になりますよ!
それでは、快適なReactライフを!分からないことがあったら、いつでもまた聞きにきてくださいね。

コメント