【React】useReducerとは何か?Reducer関数の使い方を実例で解説(state, dispatch, initialState)

react-prograshi(プロぐらし)-kv React.js
記事内に広告が含まれていることがあります。

Reactで開発を進めるうち、「useStateだけでは状態管理が複雑になってきた」「関連する複数の値をまとめて更新するのが大変」と感じたことはありませんか?

本記事では、そんな課題を解決する強力なフック、useReducerに焦点を当てています。このフックを使えば、状態(state)の更新ロジックをコンポーネントから切り離し、dispatchとreducer関数で一元管理できるようになります。

基本的な構造から、具体的なコードの実例を通じて、useReducerの使い方を解説しています。


useReducerとは何か?

useReducerは、Reactの状態(State)を管理するためのフックの一つです。

Reactで状態を扱う基本的なフックであるuseStateより高度で予測可能なものです。

状態が複雑なオブジェクトや、状態の更新ロジックが複数あり複雑な場合に、そのロジックをコンポーネント外に分離して管理するために使用されます。

useReducerを使うと、状態の更新を「アクション(Action)」という形式で統一的に行うため、状態の変化が予測しやすく、バグが入りにくいという特徴があります。

ご参考

useReducerを理解するには、useStateやstateを理解しておくことが欠かせません。詳細は下記をご参考ください。

【React】stateやuseState, setIndex, セッタ関数とは何かを実例で解説(prevの使い方)


なぜリデューサーというのか?

useReducerには「リデューサー」という言葉が入っています。

リデューサー(reducer)の本来の意味は「減らす、軽減する」です。JavaScriptにおいては、「状態を集約する」という意味を持ちます。

Reactも似ているのですが少し違って「状態の変更ロジックを一か所に『集約』する」という意味になります。

useStateの場合、状態更新のロジック(例: setCount(count + 1))は、コンポーネント内の様々なイベントハンドラ(onClickなど)に分散して書かれます。つまり、コンポーネント毎に管理しなければいけません。

そこで、useReducerを使うことで、状態の更新方法に関するすべてのロジックを、reducer関数という一つの場所に集約します。

これにより、複雑な状態を持つアプリケーションでも、「この状態がどう変わるのか」というルールブックがreducerという関数一つにまとまり、可読性と保守性が大幅に向上します。

この「状態の更新ロジックを一元管理し、次の状態に集約する」という役割から、このパターンは「Reducer」と呼ばれ、フックの名前がuseReducerとなっています。

ご参考

useReducerの元の意味となる、JavaScriptのリデューサーとは何かについては下記をご参考ください。

リデューサーとは何か?使い方や注意点を実例で分かりやすく解説



useReducerの使い方

useReducerの基本形は以下のようになっています。

import { useReducer } from 'react';

//基本形
const [state, dispatch] = useReducer(reducer, initialState);

useReducerに関数と初期値を渡し、最終的にstateとdispatchを分割代入で取得します。

ご参考

分割代入はReactを扱う上で必須です。分割代入の詳しい解説については下記をご参考ください。

> 【JavaScript】分割代入とは何か?(変数名を変える方法(変更),必要なデータだけ取り出す, デフォルト値, 多次元/ネストしたデータ)


Point

state, dispatch, reducer, initialStateのいずれも、任意の名前をつけることができます。

例えば、以下のようにすることで、現在のデータがuserDataという変数に入り、updateというdispatchにactionを渡すことで、userDataを更新します。

const [userData, update] = useReducer(countLogic, { count: 0 });

なお、初期状態は{ count: 0 }です。


主に以下の4つの要素で構成されます。

reducer関数

状態を更新するロジックを定義する純粋な関数です。

reducer(state, action)

現在のstateとactionを受け取り、新しいstateを返します。


initialState (初期状態)

コンポーネントが最初にマウントされたときの状態の初期値です。


dispatch関数

アクションを reducer に送るための関数です。

引数としてaction(=変更内容)を渡します。dispatchが呼び出されると、reducer関数が実行され、状態が更新されます。

dispatch(action)


action

更新する内容を表すオブジェクトです。

状態を更新するために必要な具体的なデータをプロパティで渡します。その際、プロパティ名は「type」と「payload」を使うことが慣習となっています。

typeとpayload

actionオブジェクトは、通常、以下の2つの主要なプロパティを持ちます。

  1. type: 何が起こったか?(どんな種類の更新をしたいか?)を識別する文字列(例: 'increment', 'addUser')。
  2. payload: その更新を実行するために必要な情報(具体的なデータ)


例えば、以下のように記述します。

動作actionオブジェクトの例payloadの内容
カウンターを特定数増やすdispatch({ type: 'add', payload: 5 })増やしたい具体的な数値 5
特定の商品をカートに追加dispatch({ type: 'ADD_ITEM', payload: { id: 101, name: 'Tシャツ' } })追加したい商品のオブジェクト全体
フォームの入力値を更新dispatch({ type: 'SET_FIELD', payload: { field: 'name', value: '山田' } })どのフィールドを、どんな値にするか


もっと簡単に!

useReducerをもっとかみ砕いて解説します。

useReducerは、「状態(State)を更新するためのルールを決めておく」ためのフックです。

useStateが「自分で直接テーブルの上の料理(State)を入れ替える」イメージだとすると、useReducerは「キッチン(Reducer)に注文(Action)を渡して、決められた手順で料理(State)を変えてもらう」イメージです。

(例えでレストランを使うのが最適かはわかりませんが、Reactの公式ページでそうしているので倣います。)


useReducerの4つの要素

Reactコンポーネントを「ホール店員」、状態(State)を「テーブルの上の料理の数や種類」だとすると、useReducerの各要素は以下の4つに分類することができます。

useReducerの要素レストランでの役割説明
state(状態)テーブルの上の料理今、カウンターに何がいくつあるか(例: {count: 0})という現在の情報。
action(行動)注文書(オーダー)どんな変更をしたいかという意図を伝えるメッセージ。
dispatch(ディスパッチ)注文を伝える行為店員が「注文書(Action)」を「キッチン(Reducer)」に渡すことです。
reducer(リデューサー)キッチン(料理のルールブック)注文書(Action)を見て、現在の料理(State)をどう変えるかを決めるシェフ。


仕組みの流れ

  1. 店員(コンポーネント)は、「ハンバーガーを追加」したいとき、直接料理を触りません。
  2. Dispatchを使って、「注文書(Action): ‘ADD_BURGER’」をキッチン(Reducer)に渡します。
  3. キッチン(Reducer)は、現在の料理の状態(State)と注文書(Action)を受け取ります。
  4. ルールブックに従い、「今のハンバーガーの数に1を足した、新しい料理の状態」を作ってホール係に戻します。
  5. ホール係は、新しい料理の状態を受け取り、テーブル(画面)を更新します。


メリット

useReducerを使うことで、状態の変更はすべてキッチン(Reducer)で一元管理され、店員(コンポーネント)は「何をしたいか(Action)」を伝えるだけで済むため、変更のルールが明確になり、コードが整理されます。


実例1:useReducerを使う流れ(カウンター)

useReducerの実際の流れを、カウンター機能を作る例で紹介します。


STEP1:初期状態の定義

初めに、stateの初期状態を定義します。カウンターを0から始めたいため、{ count: 0 }とします。

const initialState = {
  count: 0
};


STEP2:reducer関数の定義

状態を更新するロジック(カウンターを増やす、減らす、リセットする)を定義します。

reducer関数は現在のstateとactionを引数として受け取り、新しいstateを返します

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      // action.payload を使ってリセット値を受け取ることも可能
      return initialState;
    default:
      // 未知のactionが来た場合は現在のstateをそのまま返すか、エラーを投げる
      return state;
  }
}
注意点

処理の中でstateを直接変更せず、新しいオブジェクトを返します。(イミュータブルな更新)

return { count: state.count + 1 };


STEP3:コンポーネント内でuseReducerを呼び出す

コンポーネント内でuseReducerを呼び出し、stateとdispatch関数を取得します。

import { useReducer } from 'react';

function Counter() {
  // useReducer(reducer関数, 初期状態) の形式で呼び出す
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      {/* 1. stateを表示する */}
      <h1>カウンター: {state.count}</h1>

      {/* 2. ボタンクリック時に dispatch関数を呼び出す */}
      <button onClick={() => dispatch({ type: 'increment' })}>
        プラス1
      </button>

      <button onClick={() => dispatch({ type: 'decrement' })}>
        マイナス1
      </button>
      
      <button onClick={() => dispatch({ type: 'reset' })}>
        リセット
      </button>
    </div>
  );
}

// export default Counter;

「プラス1」ボタンをクリックすると、dispatchにaction、ここでは { type: ‘increment’ } を渡し、reducer関数を実行します。

return { count: state.count + 1 }; が実行され、カウンター: {state.count}の値が+1されます。


同一ファイルに記述する場合

上記カウンターを実際に使用するときは、reducerとコンポーネントを同一ファイルに記述するのが一般的です。(reducerがより複雑な場合は別ファイルに切り出しを行います)

例えば、Counter.jsxにカウンターの処理を記述し、App.jsxでそのファイルを読み込む場合は以下のようになります。

import React, { useReducer } from 'react'; //フックをインポート

// 1. 状態の初期値 (initialState) を定義
//    一般的にコンポーネントの定義前に記述します。
const initialState = {
  count: 0
};

// 2. 状態を更新するロジック (reducer) を定義
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return initialState;
    default:
      return state;
  }
}

// 3. Counterコンポーネントを定義
function Counter() {
  // useReducerを呼び出し、stateとdispatchを取得する 
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <h1>カウンター: {state.count}</h1>
 
      <button onClick={() => dispatch({ type: 'increment' })}>
        プラス1
      </button>

      <button onClick={() => dispatch({ type: 'decrement' })}>
        マイナス1
      </button>
      
      <button onClick={() => dispatch({ type: 'reset' })}>
        リセット
      </button>
    </div>
  );
}

export default Counter; // コンポーネントをエクスポート


useReducerを使ったCounterコンポーネントをルートコンポーネント(App.jsx)などで呼び出します。

import Counter from './Counter.jsx'; //Counterコンポーネントをインポート

function App() {
  return (
    <div className="App">
      <Counter /> 
    </div>
  );
}

export default App;


Reducer関数を別ファイルに切り出す場合

Reducer関数の処理が複雑な場合、Reducer関数を別ファイルに切り出すことが一般的です。

先ほどのカウンターの例で、Counterコンポーネント内の、Reducer関数をcounterReducer.jsxに切り出した場合は以下のようになります。

参考としてReducer関数の名前をcounterReducerにしておきます。

export const initialState = {
  count: 0
};

export function counterReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return initialState;
    default:
      return state;
  }
}


counterReducer.jsxの中の、initialStateとcounterReducerを分割代入で取得します。

import { useReducer } from 'react';
import { counterReducer, initialState } from './counterReducer'; 

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, initialState);

  return (
    <div>
      <h1>カウンター: {state.count}</h1>
 
      <button onClick={() => dispatch({ type: 'increment' })}>
        プラス1
      </button>

      <button onClick={() => dispatch({ type: 'decrement' })}>
        マイナス1
      </button>
      
      <button onClick={() => dispatch({ type: 'reset' })}>
        リセット
      </button>
    </div>
  );
}

export default Counter; // コンポーネントをエクスポート

reducerを別ファイルにすることで、コンポーネント(Counter.jsx)はUIの表示に集中し、ロジック(counterReducer.js)は状態の計算に集中できるため、コードの可読性がさらに向上します。

どちらの方法も可能ですが、別ファイルに分離する方がより保守性が高く、テストしやすいコード構造と言えます。


実例2:Todo リスト(追加・削除・完了)

Reducer関数とuseReducerフックを使って、Todoリストを作成することもできます。

Todoリストの状態と入力フォームの状態は別物なので、入力フォームの状態を取得する際は、useStateを使います。

import { useReducer, useState } from "react";

//1. 初期状態の定義
const initialTodos = [];

//2. Reducer関数を定義
function todoReducer(state, action) {
  switch (action.type) {
    case "ADD":
      return [...state, { id: Date.now(), text: action.text, done: false }];
    case "TOGGLE":
      return state.map((todo) =>
        todo.id === action.id ? { ...todo, done: !todo.done } : todo
      );
    case "DELETE":
      return state.filter((todo) => todo.id !== action.id);
    default:
      return state;
  }
}

export default function TodoApp() {
  const [todos, dispatch] = useReducer(todoReducer, initialTodos);
  const [text, setText] = useState(""); // ← 入力用にだけ useState を使う

  return (
    <div>
      <h1>Todo</h1>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          dispatch({ type: "ADD", text });
          setText(""); // 入力欄をクリア
        }}
      >
        <input
          value={text}
          onChange={(e) => setText(e.target.value)}
        />
        <button type="submit">追加</button>
      </form>

      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <span
              style={{ textDecoration: todo.done ? "line-through" : "none" }}
              onClick={() => dispatch({ type: "TOGGLE", id: todo.id })}
            >
              {todo.text}
            </span>
            <button onClick={() => dispatch({ type: "DELETE", id: todo.id })}>
              削除
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}
タイトルとURLをコピーしました