v19.2Latest

ステートロジックをリデューサーに抽出する

多くのイベントハンドラーに分散した多数のステート更新を持つコンポーネントは、管理が煩雑になる可能性があります。このような場合、すべてのステート更新ロジックをコンポーネントの外部にある単一の関数に統合できます。この関数はリデューサーと呼ばれます。

学習内容
  • リデューサー関数とは何か
  • useStateuseReducerにリファクタリングする方法
  • リデューサーを使用するタイミング
  • 適切なリデューサーの書き方

リデューサーでステートロジックを統合する

コンポーネントが複雑になるにつれて、コンポーネントのステートが更新されるさまざまな方法を一目で把握するのが難しくなることがあります。例えば、以下のTaskAppコンポーネントは、ステートにtasksの配列を保持し、タスクの追加、削除、編集に3つの異なるイベントハンドラーを使用しています:

各イベントハンドラーは、ステートを更新するためにsetTasksを呼び出します。このコンポーネントが成長するにつれて、そこに散らばるステートロジックの量も増加します。この複雑さを軽減し、すべてのロジックを一か所のアクセスしやすい場所にまとめるために、そのステートロジックをコンポーネントの外部にある単一の関数に移動できます。この関数は「リデューサー」と呼ばれます。

リデューサーは、ステートを扱う別の方法です。useStateからuseReducerへの移行は、3つのステップで行えます:

  1. 状態の設定からアクションのディスパッチへ移行する。
  2. リデューサー関数を記述する。
  3. コンポーネントからリデューサーを使用する。

ステップ 1: 状態の設定からアクションのディスパッチへ移行する

現在、イベントハンドラーは状態を設定することで何を行うかを指定しています:

すべての状態設定ロジックを削除します。残るのは3つのイベントハンドラーです:

  • handleAddTask(text)は、ユーザーが「追加」を押したときに呼び出されます。
  • handleChangeTask(task)は、ユーザーがタスクを切り替えるか「保存」を押したときに呼び出されます。
  • handleDeleteTask(taskId)は、ユーザーが「削除」を押したときに呼び出されます。

リデューサーによる状態管理は、状態を直接設定するのとは少し異なります。状態を設定することでReactに「何を行うか」を指示する代わりに、イベントハンドラーから「アクション」をディスパッチすることで「ユーザーが何をしたか」を指定します。(状態更新ロジックは別の場所に存在します!)したがって、イベントハンドラーを通じてtasksを「設定」するのではなく、「タスクを追加/変更/削除した」アクションをディスパッチします。これはユーザーの意図をより的確に表しています。

dispatchに渡すオブジェクトは「アクション」と呼ばれます:

これは通常のJavaScriptオブジェクトです。何を入れるかはあなたが決めますが、一般的には何が起こったかに関する最小限の情報を含めるべきです。(dispatch関数自体は後のステップで追加します。)

注記

アクションオブジェクトは任意の形状を持つことができます。

慣例として、何が起こったかを説明する文字列のtypeを与え、追加情報を他のフィールドで渡すのが一般的です。typeはコンポーネント固有のものなので、この例では'added'または'added_task'のどちらでも問題ありません。何が起こったかを表す名前を選んでください!

ステップ 2: リデューサー関数を記述する

リデューサー関数は、状態ロジックを記述する場所です。現在の状態とアクションオブジェクトの2つの引数を受け取り、次の状態を返します:

Reactは、リデューサーから返された値を状態として設定します。

この例で、状態設定ロジックをイベントハンドラからリデューサー関数に移行するには、以下の手順を行います:

  1. 現在の状態(tasks)を第一引数として宣言します。
  2. 第二引数としてactionオブジェクトを宣言します。
  3. リデューサーから次の状態を返します(Reactがこの値を状態として設定します)。

以下は、すべての状態設定ロジックをリデューサー関数に移行した例です:

リデューサー関数は状態(tasks)を引数として受け取るため、コンポーネントの外側で宣言することができます。これによりインデントレベルが減り、コードが読みやすくなることがあります。

注記

上記のコードはif/else文を使用していますが、リデューサー内部ではswitch文を使用するのが慣例です。結果は同じですが、switch文の方が一目で読みやすい場合があります。

このドキュメントの残りの部分では、以下のようにswitch文を使用します:

caseブロックを{}の中括弧で囲むことをお勧めします。これにより、異なるcase内で宣言された変数が互いに干渉するのを防げます。また、通常casereturnで終わるべきです。returnを忘れると、コードが次のcaseに「フォールスルー」してしまい、ミスを引き起こす可能性があります!

switch文にまだ慣れていない場合は、if/elseを使用しても全く問題ありません。

ステップ3: コンポーネントからリデューサーを使用する

最後に、tasksReducerをコンポーネントに接続する必要があります。ReactからuseReducerフックをインポートします:

次に、useStateを置き換えることができます:

以下のようにuseReducerに置き換えます:

このuseReducerフックはuseStateと似ています。初期状態を渡す必要があり、状態を持つ値と状態を設定する方法(この場合はdispatch関数)を返します。ただし、少し異なります。

このuseReducerフックは2つの引数を取ります:

  1. リデューサー関数
  2. 初期状態

そして、以下を返します:

  1. 状態を持つ値
  2. dispatch関数(ユーザーアクションをリデューサーに「ディスパッチ」するため)

これで完全に接続されました!ここでは、リデューサーはコンポーネントファイルの下部で宣言されています:

必要であれば、リデューサーを別のファイルに移動することもできます:

このように関心事を分離することで、コンポーネントのロジックが読みやすくなることがあります。これにより、イベントハンドラはアクションをディスパッチすることで何が起こったかを指定するだけになり、リデューサー関数がそれらに応じて状態がどのように更新されるかを決定します。

比較:useStateuseReducer

リデューサーにも欠点がないわけではありません!以下に、それらを比較するいくつかの方法を示します:

  • コードサイズ: 一般的に、useStateを使う方が最初に書くコードは少なくて済みます。useReducerでは、リデューサー関数ディスパッチアクションの両方を書く必要があります。しかし、多くのイベントハンドラーが同様の方法で状態を変更する場合、useReducerはコードを削減するのに役立ちます。
  • 可読性:useStateは、状態更新が単純な場合は非常に読みやすくなります。更新がより複雑になると、コンポーネントのコードを膨張させ、読みにくくすることがあります。そのような場合、useReducerを使うことで、更新ロジックの方法を、イベントハンドラーの何が起こったかからきれいに分離できます。
  • デバッグ: useStateでバグが発生した場合、状態がどこで誤って設定されたのか、なぜなのかを判断するのが難しい場合があります。useReducerでは、リデューサー内にコンソールログを追加して、すべての状態更新と、それがなぜ起こったのか(どのactionによるものか)を確認できます。各actionが正しければ、間違いはリデューサーのロジック自体にあることがわかります。ただし、useStateよりも多くのコードをステップ実行する必要があります。
  • テスト:リデューサーはコンポーネントに依存しない純粋関数です。つまり、個別にエクスポートして単体テストできます。一般的には、より現実的な環境でコンポーネントをテストすることが最善ですが、複雑な状態更新ロジックの場合、特定の初期状態とアクションに対してリデューサーが特定の状態を返すことを確認するのに役立ちます。
  • 個人の好み:リデューサーを好む人もいれば、そうでない人もいます。それで構いません。これは好みの問題です。useStateuseReducerは常に相互に変換できます。これらは同等です!

あるコンポーネントで状態更新の誤りによるバグに頻繁に遭遇し、そのコードにより多くの構造を導入したい場合は、リデューサーの使用をお勧めします。すべてにリデューサーを使う必要はありません。自由に組み合わせてください!同じコンポーネント内でuseStateuseReducerを併用することもできます。

リデューサーをうまく書く

リデューサーを書く際には、次の2つのヒントを心に留めておいてください:

  • Reducerは純粋でなければなりません。 状態更新関数と同様に、reducerはレンダリング中に実行されます!(アクションは次のレンダリングまでキューに入れられます。)これは、reducerが純粋でなければならないことを意味します—同じ入力は常に同じ出力になります。リクエストを送信したり、タイムアウトをスケジュールしたり、副作用(コンポーネント外のものに影響を与える操作)を実行したりしてはいけません。オブジェクト配列をミューテーションなしで更新する必要があります。
  • 各アクションは、たとえそれがデータに複数の変更をもたらす場合でも、単一のユーザー操作を記述します。例えば、reducerによって管理される5つのフィールドを持つフォームでユーザーが「リセット」を押した場合、5つの別々のset_fieldアクションではなく、1つのreset_formアクションをディスパッチする方が理にかなっています。reducer内のすべてのアクションをログに記録する場合、そのログはどのような相互作用や応答がどの順序で発生したかを再構築できるほど明確であるべきです。これはデバッグに役立ちます!

Immerを使用した簡潔なreducerの記述

通常の状態でのオブジェクトの更新配列の更新と同様に、Immerライブラリを使用してreducerをより簡潔にすることができます。ここでは、useImmerReducerを使用すると、pusharr[i] =代入で状態をミューテートできます:

Reducerは純粋でなければならないため、状態をミューテートしてはいけません。しかし、Immerはミューテートしても安全な特別なdraftオブジェクトを提供します。内部では、Immerはdraftに対して行った変更を加えた状態のコピーを作成します。これが、useImmerReducerによって管理されるreducerが最初の引数をミューテートでき、状態を返す必要がない理由です。

まとめ

  • useStateからuseReducerに変換するには:
    1. イベントハンドラーからアクションをディスパッチします。
    2. 指定された状態とアクションに対して次の状態を返すreducer関数を記述します。
    3. useStateuseReducerに置き換えます。
  • Reducerは少し多くのコードを書く必要がありますが、デバッグとテストに役立ちます。
  • Reducerは純粋でなければなりません。
  • 各アクションは単一のユーザー操作を記述します。
  • ミューテーションスタイルでreducerを書きたい場合はImmerを使用します。

Try out some challenges

Challenge 1 of 4:Dispatch actions from event handlers #

Currently, the event handlers in ContactList.js and Chat.js have // TODO comments. This is why typing into the input doesn’t work, and clicking on the buttons doesn’t change the selected recipient.

Replace these two // TODOs with the code to dispatch the corresponding actions. To see the expected shape and the type of the actions, check the reducer in messengerReducer.js. The reducer is already written so you won’t need to change it. You only need to dispatch the actions in ContactList.js and Chat.js.