React:リデューサ関数とは

リデューサ関数とは

コンポーネントが複雑になっていくと、state の更新を追跡するのが大変になる。

例えば、タスクの追加・編集・削除などがある場合、それぞれに対応するボタンにイベントハンドラを割り付け、セット関数によって更新するという流れになる:

export default function TaskApp() {
  const [tasks, setTasks] = useState(initialTasks);

  function handleAddTask(text) {
    setTasks([
      ...tasks,
      { id: nextId++, text: text, done: false, },
    ]);
  }

  function handleChangeTask(task) {
    setTasks(
      tasks.map((t) => {
        if (t.id === task.id) { return task; }
        else { return t; }
      })
    );
  }

  function handleDeleteTask(taskId) {
    setTasks(tasks.filter((t) => t.id !== taskId));
  }
  // ...略...
}

そこで使えるのがリデューサ関数で、コンポーネントの外部に関数に state ロジックを移動させることができる。

リデューサ関数への移行は次のステップで行える。

  1. state セットをアクションのディスパッチに置き換える
  2. リデューサ関数を作成する
  3. コンポーネントからリデューサを使用する

state セットのアクションをディスパッチに置き換える

リデューサは、state をセットして『何をするか』を指示するのではなく、イベントハンドラから『アクション』をディスパッチ(割り当て)して指定する。

ディスパッチへの置き換えは次の手順で進める:

  • state をセットするロジックを全て削除
  • イベントハンドラからアクションをディスパッチする
 function handleAddTask(text) {
   dispatch({
     type: 'added',
     id: nextId++,
     text: text,
   });
 }

 function handleChangeTask(task) {
   dispatch({
     type: 'changed',
     task: task,
   });
 }

 function handleDeleteTask(taskId) {
   dispatch({
     type: 'deleted',
     id: taskId,
   });
 }

ディスパッチに渡しているオブジェクトを "アクション"と呼ぶ。慣習として、何が起こったかを説明する type を与え、他のフィールドで追加情報を渡す。

typeコンポーネント固有のものとする。

リデューサ関数を作成する

リデューサ関数に state のロジックを記述する。以下の手順を踏む:

  • 現在の state を最初の引数として宣言
  • action オブジェクトを2番目の引数として宣言
  • リデューサから次の state を返す(state へのセットは React が処理する)
function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
          done: false,
        },
      ];
    }
    case 'changed': {
      return tasks.map((t) => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

上記のように、リデューサ関数は state を引数として受け取るので、コンポーネントの外部で宣言可能。

コンポーネントからリデューサを使用する

React から useReducer フックをインポートする。

useState の呼び出しを useReducer で置き換える:

const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

良いリデューサの書き方

  • リデューサは純粋である必要あり

    同じ入力に対して常に同じ出力とし、オブジェクト配列を書き換えせずに更新する。よって、リデューサの中で動的に変化するようなロジック入れない。

  • 各アクションは、複数データの更新でも単一のユーザ操作を記述する

    個々にアクションをディスパッチするよりも、1つにまとめてディスパッチする方が効率的

直接 state をセットするのとリデューサの違いのまとめ

分類 説明
state のセット イベントハンドラに『何をするか(タスク)』をセットする
リデューサ イベントハンドラに『何をしたか(アクション)』を割り当てる

ToDo アプリのコードを参考にすると、コード的には次のような違い:

// state を直接セットする場合は、何をするかをセットする
function handleAddTask(text) {
  setTasks([
    ...tasks,
    {
      id: nextId++,
      text: text,
      done: false,
    },
  ]);
}

// reducer の場合は、アクションを割り当てる
function handleAddTask(text) {
  dispatch({
    type: 'added',  // 慣習として type にアクションの説明を設定
    id: nextId++,
    text: text,
  });
}

上記の場合、reducer のロジックがないため、ロジック部分をリデューサ関数内に記述する:

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
          done: false,
        },
      ];
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

リデューサ関数では、第1引数に現在の state, 第2引数に action オブジェクトを渡し、リデューサ関数では 次の state が戻り値になる。

参考