React の状態管理について
状態管理は React で開発したアプリケーションの中心的な部分を担っています。
状態とは、アプリケーションが変化する可能性のあるデータを指します。例えばユーザー操作によるインタラクション、WebAPI から取得するデータ、フォームの入力など、様々なものを含みます。
React では、それらの状態管理を実現するにあたって複数のパターンがあり、それぞれメリットとデメリットがあります。 そこで、下記の状態管理についてその特徴を整理しました。
- useState
- useContext
- Redux(Redux Toolkit)
- Recoil
useState について
React では、useState というフックを使って状態を管理することができます。useState は、関数コンポーネント内で状態を持つことを可能にするフックとなります。
https://react.dev/reference/react/useState
useState で状態管理したとき、その状態が更新されると関連するコンポーネントが自動的に再レンダリングされて、状態の変更がユーザーインターフェースに反映されます。
useState は特定のコンポーネント内で状態を管理するときに使用します。場合によっては親コンポーネントから子コンポーネントの 2 つのコンポーネント間で状態を props として渡すことがあります。
useState の限界
useState は特定のコンポーネントに限定されている場合には非常に役立ちますが、複雑な状態管理には適していません。
useState として設定した状態を、親コンポーネントから子コンポーネントへ props として渡していくことをバケツリレーと言います。
これがコンポーネント同士で近しければ問題は大きくなりませんが、多くのコンポーネントを経由すると管理が複雑になります。
例えば、あるコンポーネントが状態を持っていて、その状態がそのコンポーネントの従兄弟や再従兄弟に当たるコンポーネントで必要となる場合、それらに至るまでのコンポーネントを通して状態を渡さなければなりません。
これは非効率的であり、保守性や可読性にも影響を与えます。
useContext について
useContext は、React の組み込みフックで、状態をコンポーネントツリー全体で共有します。
https://react.dev/reference/react/useContext
特徴として、状態を子孫コンポーネントに直接渡すことができるので、useState でみたバケツリレーの問題を解決します。
さらに useContext を使用することで、関連する状態を一箇所にまとめることができ、保守性や可読性が向上します。
使い方は、React.createContext を用いてコンテキストを作成し、それを適切にプロバイダー(<MyContext.Provider>)とコンシューマー(useContext(MyContext))でラップします。
なお、提供されたコンテキストが更新されると、コンテキストプロバイダーでラップされた全てのコンポーネントが自動的に再レンダリングされません。あくまでもコンテキストを useContext で参照しているコンポーネントのみ再レンダリングされます。
useContext をいつ使用すべきか
useContext は、ある React コンポーネントのツリーに対して「グローバル」とみなすことができる、現在の認証済みユーザ・テーマ・優先言語といったデータを共有するために設計されています。
useContext の限界
異なる状態を管理するため複数のコンテキストを作成するとき、複数の<MyContext.Provider>をラップする必要があり、コードの保守性や可読性に問題が生じます。
加えて、<MyContext.Provider>を適切に管理する必要があり、呼び出し元と呼び出し先の両方の構造を各開発者が理解していないと予期しないエラー、ならびに混乱を引き起こします。
Redux について
Redux は JavaScript アプリケーションのための予測可能な状態コンテナです。
https://redux.js.org/
主に React や React Native と組み合わせて使用され、アプリケーションで扱う全ての状態を一元化し、管理を容易にします。状態を一元化して管理する点で useContext と似ていますが、Redux で状態を管理しているのはコンポーネントツリーとは分離した Store になります。
では、その Store で管理している状態を更新するまでのデータフローについて。
データフローは一方向性で成り立っています。
- Actionの dispatch:例えば、ユーザーがボタンをクリックしたとき、あるいは画面表示時に Web API からデータをリクエストすることをトリガーに、View 側で Action を dispatch します。 Action はプレーンな JavaScript オブジェクトで、どのような操作が行われたのかを示す type フィールドを必ず持っています。
- Reducerの実行:Redux の Store は、Action が dispatch されるたびに、Action と現在のアプリケーションの状態を引数として Reducer を呼びます。 Reducer は純粋な関数で、前の状態と Action を受け取り、新しい状態を返します。
- Storeの更新:Reducer が新しい状態を返すと、Store は新しい状態に変更されます。
- View の更新:Store の状態が変更されると、新しい状態がアプリケーションの UI に反映されます。
以上のステップが一方向に進行するため、Redux のデータフローは予測可能で、デバッグやテストが容易になります。
さらに、一方向のデータフローはコードの可読性と保守性を向上させます。
Redux-Saga、Redux-Thunk で非同期処理を扱う
Redux-saga、Redux-Thunk は、Redux アプリケーションで非同期処理(例えば、WebAPI でデータの取得)を扱うためのミドルウェアです。
- https://redux-saga.js.org/
- https://redux.js.org/usage/writing-logic-thunks
それらを使用した場合の処理は、Redux のデータフローにおける Action と Reducer の間で行われます。
すなわち、View 側で Action を dispatch した後、非同期処理が行われて完了したら、その結果(成功または失敗)を含む新しい Action を dispatch します。そうすると Action と現在のアプリケーションの状態を引数として Reducer が呼ばれて、Store の更新、View の更新へと処理が行われます。
Redux Toolkit について
Redux Toolkit は Redux をより簡単で、効率的に使用するために開発されたライブラリです。
前述の Redux の特性を理解した上で、Redux Toolkit を使うことが推奨されています。よって、アプリケーションで扱う全ての状態を一元で管理するとき、Redux Toolkit を使用するのが良いでしょう。
Recoil について
Recoil は React 専用の状態管理ライブラリで、前述の Redux とは大きく異なる点があります。
https://recoiljs.org/
- 設計思想:Recoil はアプリケーションの状態を小さな単位(Atom)として分割し、それぞれを独立して管理することができます。(分散管理) 一方、Redux では 1 つの Store に状態を一元化します。(一元管理)
- データフロー:Recoil では、状態(Atom)を購読しているコンポーネントだけが再レンダリングされます。これにより、不必要な再レンダリングを減らし、パフォーマンスを改善することが可能となります。 一方、Redux では Action が dispatch されると、全てのコンポーネントが新しい状態との差分を計算して、再レンダリングが必要かどうかを判断します。
- 非同期処理:Recoil では selector という概念を利用して非同期処理を扱います。 selector は Atom の値に基づいて計算され、その結果は Recoil の状態としてキャッシュされるため、非同期データの取得や状態の取り回しが容易となります。 一方、Redux では非同期処理を扱うためには前述したミドルウェアが必要です。
なお、現時点で Recoil の最新 v が 0.7.7 で正式リリースされてない点と、ライブラリサイズが重い点がデメリットとして挙げられます。
以上となります。
簡単ではありますが、React で状態管理を扱うためのパターンとそれぞれの特徴について整理しました。