ReducerとContextによるスケールアップ
Reducerはコンポーネントの状態更新ロジックを統合できます。Contextは情報を他のコンポーネントの深くまで渡すことができます。ReducerとContextを組み合わせることで、複雑な画面の状態を管理できます。
学習内容
- ReducerとContextを組み合わせる方法
- 状態とdispatchをprops経由で渡さない方法
- Contextと状態ロジックを別ファイルに保持する方法
ReducerとContextの組み合わせ
この例はReducerの導入からのもので、状態はReducerによって管理されています。Reducer関数にはすべての状態更新ロジックが含まれており、このファイルの下部で宣言されています:
リデューサーはイベントハンドラーを短く簡潔に保つのに役立ちます。しかし、アプリが成長するにつれて、別の困難に直面するかもしれません。現在、tasks状態とdispatch関数は、最上位のTaskAppコンポーネントでのみ利用可能です。他のコンポーネントがタスクのリストを読み取ったり変更したりできるようにするには、現在の状態とそれを変更するイベントハンドラーを明示的にpropsとして渡す必要があります。
例えば、TaskAppはタスクのリストとイベントハンドラーをTaskListに渡します:
そして、TaskListはイベントハンドラをTaskに渡します:
このような小さな例ではうまく機能しますが、中間に数十や数百のコンポーネントがある場合、すべての状態と関数を渡すのは非常に煩わしくなる可能性があります!
これが、propsを通じて渡す代わりに、tasks状態とdispatch関数の両方をコンテキストに入れることを検討する理由です。TaskApp以下のツリー内の任意のコンポーネントは、繰り返しの「プロップドリリング」なしでタスクを読み取り、アクションをディスパッチできます。
以下は、リデューサーとコンテキストを組み合わせる方法です:
- コンテキストを作成する。
- 状態とディスパッチをコンテキストに配置する。
- ツリー内のどこでもコンテキストを使用する。
ステップ 1: コンテキストを作成する
useReducerフックは、現在のtasksと、それらを更新できるdispatch関数を返します:
これらをツリーの下に渡すには、2つの別々のコンテキストを作成します:
TasksContextは現在のタスクリストを提供します。TasksDispatchContextは、コンポーネントがアクションをディスパッチできる関数を提供します。
後で他のファイルからインポートできるように、別のファイルからエクスポートします:
ここでは、両方のコンテキストのデフォルト値としてnullを渡しています。実際の値はTaskAppコンポーネントによって提供されます。
ステップ 2: 状態とディスパッチをコンテキストに配置する
これで、TaskAppコンポーネントで両方のコンテキストをインポートできます。useReducer()によって返されるtasksとdispatchを取り、それらを下のツリー全体に提供します:
現時点では、情報をpropsとコンテキストの両方で渡しています:
次のステップでは、propsの受け渡しを削除します。
ステップ3: ツリー内のどこでもコンテキストを使用する
これで、タスクのリストやイベントハンドラをツリーの下に渡す必要はありません:
代わりに、タスクリストが必要なコンポーネントは、TasksContextから読み取ることができます:
タスクリストを更新するには、どのコンポーネントもコンテキストからdispatch関数を読み取り、それを呼び出すことができます:
TaskAppコンポーネントはイベントハンドラを下に渡さず、TaskListもイベントハンドラをTaskコンポーネントに渡しません。各コンポーネントは必要なコンテキストを読み取ります:
状態は依然として最上位のTaskAppコンポーネントに「存在」し、useReducerで管理されています。しかし、そのtasksとdispatchは、これらのコンテキストをインポートして使用することで、ツリー内の下位のすべてのコンポーネントで利用可能になりました。
すべての配線を1つのファイルに移動する
これは必須ではありませんが、リデューサーとコンテキストの両方を1つのファイルに移動することで、コンポーネントをさらに整理することができます。現在、TasksContext.jsには2つのコンテキスト宣言しか含まれていません:
このファイルはすぐに混雑することになります!リデューサーを同じファイルに移動します。次に、同じファイル内に新しいTasksProviderコンポーネントを宣言します。このコンポーネントはすべての要素を結びつけます:
- リデューサーを使用して状態を管理します。
- 下位のコンポーネントに両方のコンテキストを提供します。
- JSXを渡せるように、childrenをプロップとして受け取ります。
これにより、TaskAppコンポーネントからすべての複雑さと配線が取り除かれます:
また、useコンテキストを使用する関数をTasksContext.jsからエクスポートすることもできます:
コンポーネントがコンテキストを読み取る必要がある場合、これらの関数を通じて行うことができます:
これは動作を何ら変えるものではありませんが、後でこれらのコンテキストをさらに分割したり、これらの関数にロジックを追加したりできるようにします。これで、すべてのコンテキストとリデューサーの配線がTasksContext.jsに含まれるようになりました。これにより、コンポーネントはクリーンで整理された状態を保ち、データをどこから取得するかではなく、何を表示するかに集中できます:
以下のように考えることができます:TasksProviderはタスクの扱い方を知っている画面の一部、useTasksはそれらを読み取る方法、そしてuseTasksDispatchはツリー内の下位にある任意のコンポーネントからそれらを更新する方法です。
注記
関数useTasksやuseTasksDispatchはカスタムフックと呼ばれます。関数名がuseで始まる場合、それはカスタムフックとみなされます。これにより、その内部でuseContextのような他のフックを使用できます。
アプリが成長するにつれて、このようなコンテキストとリデューサーのペアが多数存在する可能性があります。これは、ツリーの深い場所にあるデータにアクセスしたいときに、多くの作業をせずにアプリをスケールさせ、状態を持ち上げるための強力な方法です。
まとめ
- リデューサーとコンテキストを組み合わせることで、任意のコンポーネントがその上位の状態を読み取り、更新できるようにすることができます。
- 下位のコンポーネントに状態とdispatch関数を提供するには:
- 2つのコンテキスト(状態用とdispatch関数用)を作成します。
- リデューサーを使用するコンポーネントから両方のコンテキストを提供します。
- それらを読み取る必要があるコンポーネントからいずれかのコンテキストを使用します。
- すべての配線を1つのファイルに移動することで、コンポーネントをさらに整理することができます。
- コンテキストを提供する
TasksProviderのようなコンポーネントをエクスポートできます。 - また、
useTasksやuseTasksDispatchのようなカスタムフックをエクスポートして読み取ることもできます。
- コンテキストを提供する
- アプリ内にこのようなコンテキストとリデューサーのペアを複数持つことができます。
