useReducer完全入門──複雑な状態管理をスマートに解決するReactフックの使い方

React

なぜuseReducerが必要か

useStateは直感的で便利ですが、

  • 複数の状態が相互に影響する
  • 更新ロジックが長い if/else で肥大化する
  • 次の状態が前の状態に依存する更新が多い

といった場面では読みづらくなります。こうしたケースでuseReducerを使うと、状態更新ロジックを1カ所に集約でき、コンポーネントがスッキリします。

基本シグネチャ

const [state, dispatch] = useReducer(reducer, initialState, init?)
パラメータ意味
reducer(state, action) ⇒ newState更新ロジックを持つ純粋関数
initialState任意初期値
init(optional) (arg) ⇒ initializedState遅延初期化用

戻り値

  • state:現在の状態
  • dispatch:アクションを送る関数

まずはカウンターで理解

import { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    case 'decrement': return { count: state.count - 1 };
    default: throw new Error();
  }
}

export default function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return (
    <>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}></button>
      <button onClick={() => dispatch({ type: 'decrement' })}></button>
    </>
  );
}

ポイント解説

  • reducer純粋関数。副作用を入れない
  • dispatch へは オブジェクトを渡す(type必須)
  • UI 側は state を読むだけ=ロジックが分離され可読性↑

複数フィールドのフォーム例

useStateでそれぞれ管理すると更新がバラけますが、useReducerで一元化可能。

function formReducer(state, action) {
  switch (action.type) {
    case 'change':
      return { ...state, [action.field]: action.value };
    case 'reset':
      return action.initial;
    default:
      return state;
  }
}

const initialForm = { name: '', email: '', password: '' };

function SignupForm() {
  const [form, dispatch] = useReducer(formReducer, initialForm);

  const handleChange = e =>
    dispatch({ type: 'change', field: e.target.name, value: e.target.value });

  return (
    <form>
      {Object.entries(form).map(([key, val]) => (
        <input key={key} name={key} value={val} onChange={handleChange} />
      ))}
      <button onClick={() => dispatch({ type: 'reset', initial: initialForm })}>
        クリア
      </button>
    </form>
  );
}

深掘り

  • ...state で既存をコピー=不変性を守る
  • resetアクションでワンアクション全項目初期化
  • 大量フィールドでも reducer が変わらないので拡張に強い

遅延初期化でパフォーマンス改善

重い計算で初期値を作る場合、第三引数 init を使うとレンダー時に毎回呼ばれません。

function init(countFromLocal) {
  return { count: countFromLocal };
}
const [state, dispatch] = useReducer(reducer, initialArg, init);

useState vs useReducer 使い分け指針

シチュエーション推奨フック
1〜2個の単純な値useState
更新ロジックが直感的useState
多数のフィールド・複雑な分岐useReducer
次の状態が前の状態に強く依存useReducer
Redux 風のアーキテクチャを局所的に実現useReducer

コンポーネント分割との相性

Reducer を別ファイルに切り出し、複数コンポーネントから共通利用するとReduxライクな構成が得られます。

// todoReducer.js
export function todoReducer(state, action) { /* 省略 */ }

// TodoList.jsx
import { todoReducer } from './todoReducer';
const [todos, dispatch] = useReducer(todoReducer, []);

まとめ

useReducer状態更新ロジックを一元管理し、可読性と拡張性を両立させる強力なフックです。

  1. 純粋関数 reducer を定義
  2. dispatch で明示的にアクションを送る
  3. ステートが複雑化したら useReducer へ移行

ロジックが整理されることでバグ発生率が減り、大規模アプリでもスケールしやすいコードベースを実現できます。

コメント