🤖

[初心者向]Reactスターターはここから始めろ!その4:リデューサ関数とは

2024/11/23に公開

当記事はJavaScriptもReactもそんなに詳しくないよって人向けです。

↓↓前記事↓↓
https://zenn.dev/kkyoka/articles/8777d7d06515f6

React公式ドキュメントの「Reactを学ぶ」のstateの管理のところを読んでいきます。
以下はその中の「state ロジックをリデューサに抽出する」のまとめ。

リデューサ関数の概要

stateセッタ関数がさまざまなところで呼ばれるようになると、コードの可読性が悪くなります。
例えばタスクの管理をするページでstate変数にtasksという配列を持っていたとします。
tasksが変更されるトリガーは以下の3つがあります。

  • taskの追加
  • taskの編集
  • taskの削除

それぞれでstateセッタ関数を呼び出すとstateがいつどのように更新されるかが一目では分からなくなります🤔

そこでリデューサ関数を使います。

リデューサ関数はstate変数と「アクション」を受け取り、アクションに合わせてstate変数を更新し、その結果を返します。
Reactはその結果をstateに反映します。

リデューサ関数を使うことで、「ユーザーのアクション」と「state変数への処理」を切り離して考えることが可能になります。

以下は「タスクを追加」ボタン押下時にtasksに追加される処理をリデューサに移したものです。

// useReducerをimport
import { useReducer,useState  } from 'react';

// tasksの初期値をセット
const initialTasks = [
    {id: 0, text: 'タスク1'},
    {id: 1, text: 'タスク2'},
    {id: 2, text: 'タスク3'}
];
let nextId = 3;

export default function App() {
    const [tasks, dispatch] = useReducer(tasksReducer, initialTasks); // useStateの代わりにuseReducerを使う
    const [text, setText] = useState(''); // 入力テキスト管理用

    // タスク追加のイベントハンドラ
    function handleAddTask() {
        // dispatchにaddedを渡して処理をしてもらう
        dispatch({
            type: 'added',
            id: nextId++,
            text: text
        });
        setText(''); // 入力欄をリセット
    }

    return (
        <>
            <h1>タスク管理</h1>
            <input placeholder="タスクを追加" onChange={(e) => setText(e.target.value)}/>
            <button onClick={handleAddTask}>タスクを追加</button>
            <TaskList tasks={tasks}/>
        </>
    );
}

function TaskList({ tasks }) {
    return (
        <>
            {tasks.map((task) => (
                <div>{task.id}{task.text}</div>
            ))}
        </>
    )
}

// リデューサ関数の定義
function tasksReducer(tasks, action) {
  switch (action.type) {
    // addedのアクションが起きた時の処理
    case 'added': {
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
        },
      ];
    }
    default: {
      throw Error('定義されていないアクションです:' + action.type);
    }
  }
}

リデューサ関数の書き方

  • リデューサ関数を定義
    • 第1引数にstate変数を、第2引数にアクションオブジェクトを受け取り、処理後のstate変数を返す
    • function tasksReducer(tasks, action) {
          // action.typeごとに処理を記載
      }
      
  • useReducerをimport
    • import { useReducer } from 'react';
      
  • useReducerフックを呼び出す
    • リデューサ関数とstate変数の初期値を受け取り、state値とそれを更新するための手段(dispatch)を返す
    • const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
      
  • ハンドラでdispatchを呼び出す
    • function handleAddTask() {
        dispatch({
            type: 'added',
            id: nextId++,
            text: text
        });
      }
      

リデューサ関数のメリット、デメリット

メリット

  • 「何が起きたのか」「何を更新するのか」を分離することができる
  • 複雑な処理の場合は、可読性が上がる
  • state変数を更新する処理が1箇所にまとまっているため、デバッグがしやすい
  • リデューサはコンポーネントに依存しないため、コンポーネント単体のテストがしやすくなる

デメリット

  • useStateを使うよりもコード量が多くなる
  • シンプルな処理の場合はuseStateの方が可読性が高い

リデューサ関数の注意点

  • リデューサ関数は純関数である必要がある
    • リデューサ関数はレンダー中に実行される
    • そのためオブジェクトや配列の書き換えはNGです
  • ひとつの操作をひとつのアクションとする
    • たとえば「リセットボタン押下時に5つのフィールドをリセットする」としたとき、フィールドそれぞれでdispatchすることもできます
    • ただそれだとユーザーの操作とそれに対するレスポンスを紐づけることができません
    • ユーザーの操作ごとに処理をひとまとめにすることで順序が追いやすくなり、ログやデバッグがしやすくなります

感想

読んでるだけだとまじで1割くらいしか頭に入ってない。
とにかく書かないとダメ。

参考

いただいたコメント

今回の例だと、textをstateに入れる必要はなさそうです。下記のような感じでもいいかな?と思いました。
また、playgroundで動作させてみたら、linterも動くみたいで、taskListを作る関数のdivタグにkeyを入れてくれと言われたので、そちらも修正しました。
https://playcode.io/react

Discussion