こんにちは。毎日チケットを消化しながら「また`useState`の値が古い……?」とデバッガーの前で頭を抱えていないかい?
中級の壁を突破してシニアの領域に踏み込もうとしている君なら、フォームの入力値が複雑に入り組んだり、非同期処理の絡む複雑な画面状態を前にして、`useState`の散らかった更新処理に限界を感じた経験が一度や二度ではないはずだ。「前の状態を元に次の状態を計算したいのに、レンダリングサイクルとバッチ処理のせいで値がズレる……」なんてね。
今回は、そんな地獄のような状態管理から君を解放し、コードのメンテナンスタンスを劇的に引き上げる`useReducer`について、実務の現場で本当に役立つ知見を交えて徹底的に解説しよう。
—
なぜ `useState` では限界が来るのか?
実務で少し複雑なUI(例えば、非同期のバリデーション、複数のステップがあるウィザード形式のフォーム、ショッピングカートの複雑な数量・割引計算など)を実装すると、ひとつのコンポーネント内にこんなコードが乱立し始める。
// ありがちな散らかったuseStateの山
const [step, setStep] = useState(1);
const [formData, setFormData] = useState({});
const [isLoading, setIsLoading] = useState(false);
const [errors, setErrors] = useState({});
// これらを連動させて更新しようとすると……
const handleNextStep = async () => {
setIsLoading(true);
try {
const isValid = await validate(formData);
if (isValid) {
setStep(prev => prev + 1);
setErrors({});
} else {
setErrors({ general: ‘入力内容に不備があります’ });
}
} catch (e) {
// エラーハンドリング……
} finally {
setIsLoading(false);
}
};
見ていて胸焼けがしそうだろう?
これの何が問題って、「状態がどう変化するのか(状態遷移のルール)」が、イベントハンドラやエフェクトの中に散らばってしまうことだ。ビジネスロジックがUIの配管工事と密結合してしまい、後から仕様変更が入ったときにデバッグの旅に出る羽目になる。
ここで登場するのが `useReducer` だ。
—
useReducer の基本構造:頭の中を整理するメンタルモデル
`useReducer` は、Reduxの思想をReactのローカルステートに持ち込んだもの……と聞くと難しく聞こえるかもしれないが、要するにこうだ:
「コンポーネントの外(あるいは内部の純粋関数)に、状態の『取扱説明書(Reducer)』を置き、コンポーネントからは『リクエスト(Action)』だけを投げる」
基本構文を見てみよう。
const [state, dispatch] = useReducer(reducer, initialState);
登場人物はたったの3つ。
1. `state`: 現在の状態。
2. `dispatch`: 「こういうことが起きた!」とReactに伝えるための連絡係(関数)。
3. `reducer`: 「こういう事件(Action)が起きたなら、現在の状態をこう書き換える」というルールを定義した純粋関数。
1. 脳裏に焼き付けてほしい Reducer のシグネチャ
function reducer(state, action) {
// 必ず新しい状態オブジェクトを返却する(イミュータブルに!)
switch (action.type) {
case ‘EVENT_NAME’:
return { …state, targetProperty: action.payload };
default:
throw new Error(`予期しないアクションタイプです: ${action.type}`);
}
}
ここで重要なのは、`reducer` は必ず「純粋関数(Pure Function)」でなければならないという点だ。同じ入力(state と action)を与えれば、絶対に同じ出力(新しい state)を返すこと。APIコールを叩いたり、現在時刻(`Date.now()`)やランダムな値をその場で生成したりしてはいけない。副作用はすべてコンポーネント側(イベントハンドラや `useEffect`)で処理し、reducerには「純粋な計算」だけをさせろ。これがバグを生み出さないための鉄則だ。
—
現場で即戦力になる実装パターン
百聞は一見に如かず。実務でよくある「ユーザー情報の編集フォーム(ローディング・エラー・データ管理)」を `useReducer` で美しく書き換えたサンプルコードを見てほしい。そのままコピーして挙動を確認できるように書いておいた。
import React, { useReducer } from ‘react’;
// 1. 初期状態を定義する(型や構造を明確に!)
const initialState = {
name: ”,
email: ”,
isLoading: false,
error: null,
isSaved: false,
};
// 2. Reducer関数を定義(コンポーネントの外に置くのがベストプラクティス)
function userFormReducer(state, action) {
switch (action.type) {
case ‘FIELD_CHANGED’:
return {
…state,
// 計算プロパティ名を使って、動的にフィールドを更新
[action.field]: action.value,
isSaved: false, // 編集されたら保存済みフラグを落とす
};
case ‘SUBMIT_START’:
return {
…state,
isLoading: true,
error: null,
};
case ‘SUBMIT_SUCCESS’:
return {
…state,
isLoading: false,
isSaved: true,
};
case ‘SUBMIT_FAILURE’:
return {
…state,
isLoading: false,
error: action.payload, // エラーメッセージを格納
};
case ‘RESET’:
return initialState;
default:
// 万が一、未知のアクションが来たらバグに気づけるように例外を投げるか、コンソールに警告を出す
throw new Error(`Unhandled action type: ${action.type}`);
}
}
export function UserProfileForm() {
// 3. useReducerのフックを呼び出す
const [state, dispatch] = useReducer(userFormReducer, initialState);
// 入力値変更のハンドラ
const handleChange = (e) => {
const { name, value } = e.target;
// dispatchには「何をしたいか(type)」と「そのデータ(payload)」を渡す
dispatch({
type: ‘FIELD_CHANGED’,
field: name,
value: value,
});
};
// フォーム送信のシミュレーション(非同期処理)
const handleSubmit = async (e) => {
e.preventDefault();
dispatch({ type: ‘SUBMIT_START’ });
try {
// 実際のAPI通信のつもり
await new Promise((resolve, reject) => {
setTimeout(() => {
if (state.email.includes(‘error’)) {
reject(new Error(‘サーバーエラーが発生しました’));
} else {
resolve();
}
}, 1000);
});
dispatch({ type: ‘SUBMIT_SUCCESS’ });
} catch (err) {
dispatch({ type: ‘SUBMIT_FAILURE’, payload: err.message });
}
};
return (
);
}
—
現場のシニアが教える「dispatch」の裏側とパフォーマンスの極意
ここで少し、Reactが内部でどう動いているかという裏側の話をしておこう。
`dispatch` 関数を呼び出したとき、Reactは即座に画面を再描画するわけではない。React 18の標準である自動バッチング(Automatic Batching)の仕組みにより、ひとつのイベントループ内で複数回 `dispatch` が呼ばれても、レンダリングは最後に1回にまとめられる。
また、ここがシニアとしての重要なTipsだが、`dispatch` 関数のアイデンティティ(参照の同一性)は、Reactのライフサイクルを通じて完全に安定している(保証されている)。
つまり、`useCallback` の依存配列に `dispatch` を含める必要は一切ないし、子コンポーネントに `dispatch` をプロパティとしてガンガン渡しても、不要な子コンポーネントの再レンダリングを引き起こさない。Reduxの `dispatch` と同じく、非常にパフォーマンス上有利に設計されているんだ。
いつ `useState` から `useReducer` に移行すべきか?
なんでもかんでも `useReducer` を使えばいいというわけではない。単なるひとつの真偽値(`const [isOpen, setIsOpen] = useState(false)`)を管理するために冗長なreducerを書くのは、ただのボイラープレート(無駄なコード)の増加だ。
移行の判断基準はこれだ:
1. 状態の更新が「他の複数の状態」に依存しているとき(例:Aが変更されたらBとCをリセットし、Dをトグルする)
2. 状態の遷移ルール(ビジネスロジック)が複雑で、テストを書きたいとき(Reducer関数は純粋関数なので、Reactのコンポーネントをレンダリングしなくても単体テスト(Jest/Vitestなど)が非常に書きやすい!)
3. 3つ以上の関連する `useState` が同じイベントハンドラ内で同時に更新されているとき
このラインを超えたら、迷わず `useReducer` を採用してほしい。コードの保守性が劇的に変わることを保証しよう。
—
さあ、今日の業務からその散らかった `useState` の山を整理整頓してみよう。美しいアーキテクチャは、日々の地道なリファクタリングの積み重ねから生まれるんだ。健闘を祈る!

コメント