NgRx の基本要素:ストア、アクション、リデューサー、セレクタ、エフェクト
ストアは、状態管理プロセス全体における重要な要素です。 これは状態を保持し、コンポーネントと状態の間の相互作用を促進する。 以下に示すように、Angular の依存性注入によってストアへの参照を取得することができます。
constructor(private store: Store<AppState>) {}
このストア参照は、その後 2 つの主要操作に使用することができます。
-
store.dispatch(…)
メソッドを使用してストアにアクションをディスパッチし、次にリデューサと効果をトリガする - セレクタを介してアプリケーション状態を取得する
状態オブジェクト ツリーの構造
あなたのアプリケーションが User と Product という 2 つの機能モジュールからなると仮定します。 これらのモジュールのそれぞれは、全体の状態の異なる部分を処理します。 Productの情報は常に状態のproducts
セクションに保持されます。 ユーザー情報は常にステートのuser
セクションに保持されます。 これらのセクションはスライスとも呼ばれます。
Actions
アクションとはストアに対してディスパッチする命令で、必要に応じていくつかのメタデータ (payload) とともにディスパッチすることもできます。 アクションの種類に基づいて、ストアはどのオペレーションを実行するかを決定します。 コードでは、アクションは2つの主要な属性、すなわちtype
とpayload
を持つ、古いJavaScriptオブジェクトで表されます。 payload
はオプション属性で、リデューサが状態を変更するために使用されます。 次のコード スニペットと図は、この概念を示しています。
{ "type": "Login Action", "payload": { userProfile: user }}
NgRx バージョン 8 には、アクション クリエーター(アクションではなく、アクション クリエーター)を定義する createAction
というユーティリティ関数が用意されています。 7589>
そして、login
アクションクリエーター(これは関数です)を使って、以下のようにアクションを構築し、ストアにディスパッチすることができます。 user
はアクションに渡す payload
オブジェクトです。
this.store.dispatch(login({user}));
Reducers
Reducers は状態を変更し、変更を加えた新しい状態オブジェクトを返す役割を担っている。 レデューサーは2つのパラメータ、現在の状態とアクションを受け取ります。 受け取ったアクションの種類に基づいて、レデューサーは現在の状態に対して特定の修正を実行し、新しい状態を生成します。 この概念は、下の図で紹介されています。
アクションと同様に、NgRx には、reducers を作成する createReducer
というユーティリティ関数が用意されています。 典型的な createReducer
関数呼び出しは次のようになります。
見てわかるように、これは初期状態 (アプリケーション起動時の状態) と 1 対多の状態変更関数を取り込み、異なるアクションにどのように反応するかを定義しています。 これらの各状態変更関数は、現在の状態とアクションをパラメーターとして受け取り、新しい状態を返します。
Effects
エフェクトにより、アクションがストアにディスパッチされるときに副作用を実行することができます。 例を通してこれを理解しようとします。 ユーザーがアプリケーションへのログインに成功すると、type
Login Action
のアクションが payload
にユーザー情報を格納してストアにディスパッチされる。 reducer関数はこのアクションをリスニングし、ユーザー情報を含むstateを変更します。 さらに、副次的な効果として、ブラウザのローカルストレージにユーザー情報を保存したい場合もあります。 エフェクトはこの追加タスク(副作用)を実行するために使用できます。
NgRxでエフェクトを作成する方法は複数あります。 以下は、効果を作成する生で自明な方法です。 一般的に、この方法を使用して効果を作成することはないことに注意してください。
-
actions$
observable は、ストアから受け取ったアクションを発行します。 これらの値は演算子チェーンを通過します。 -
ofType
が最初に使用される演算子です。 これは、NgRx (Not RxJS) が提供する特殊な演算子で、アクションをそのタイプに基づいてフィルタリングするためのものです。 この例では、login
タイプのアクションのみが残りの演算子チェーンを通過することができます。 -
tap
は、ブラウザのローカルストレージにユーザー情報を保存するためにチェーン内で使用される2番目の演算子です。tap
演算子は一般に、演算子チェーンで副作用を実行するために使用されます。 - 最後に、手動で
login$
observable を購読しなければなりません。
しかしながら、このアプローチにはいくつかの大きな欠点があります。
- observable に手動で購読しなければならない。 この方法では、常に手動で購読を解除しなければならず、保守性の欠如につながります。
- Operator チェーンでエラーがポップアップした場合、observable はエラーになり、その後の値 (action) を出すのを止めます。 その結果、副作用が実行されなくなります。
これらの問題を克服するために、NgRx は効果を作成するための createEffect
というユーティリティ関数を提供します。 典型的な createEffect
関数呼び出しは次のようになります。
createEffect
メソッドは observable と (オプションで) 設定オブジェクトをパラメーターとして返す関数を取り込みます。
サポート関数が返す observable への加入は NgRx によって行われるので、加入または脱退を手動で行う必要がありません。 また、オペレータチェーンで何らかのエラーが発生した場合、NgRx は新しい observable を作成し、副作用が常に実行されるように再購読します。
設定オブジェクトの dispatch
が true
(デフォルト値) であれば、createEffect
メソッドは Observable<Action>
を返します。 それ以外の場合はObservable<Unknown>
が返されます。 dispatch
プロパティが true
の場合、NgRx は type
Observable<Action>
の返された observable を購読し、受け取ったアクションをストアにディスパッチします。
もし受け取ったアクションをオペレータチェーンで別のタイプのアクションにマッピングしない場合は、dispatch
に false
をセットしなければならないでしょう。 そうしないと、同じアクションが何度もactions$
ストリームにディスパッチされて受信されるため、実行は無限ループになります。 たとえば、以下のコードでは、元のアクションを演算子チェーン内の別のタイプのアクションにマッピングしているため、dispatch
を false
に設定する必要はありません。
上記のシナリオでは、
- Effect は
type
loadAllCourses
のアクションを受信しています。 - API が呼び出され、コースが副作用としてロードされます。
-
type
allCoursesLoaded
のアクションへの API 応答がマッピングされ、ロードされたコースがアクションにpayload
として渡されます。 これは、フードの下の NgRx によって行われます。 - Rducer は、受信した
allCoursesLoaded
アクションを聞き、ロードされたコースで状態を変更します。
Selectors
Selectors は、ストア状態のスライスを得るために使用する純粋な関数です。 以下に示すように、セレクタを使用しなくても、状態をクエリすることができます。
const isLoggedIn$ = this.store.pipe(map(state => !!state.user));
-
store
は購読可能な観測値です。 ストアがアクションを受け取るたびに、store
はそのサブスクライバーに状態オブジェクトをプッシュします。 - 状態のスライスを取得し、必要に応じて任意の計算を実行するために、マッピング関数を使用することができます。 上の例では、ステート オブジェクト ツリーの
user
スライスを取得し、ユーザーがログインしたかどうかを判断するためにブール値に変換しています。 -
isLoggedIn$
observable を手動で購読するか、非同期パイプで Angular テンプレートに使用して、発行される値を読み取ることができます。 一般に、ストアはアプリケーションのさまざまな部分から頻繁にアクションを受け取ります。 上記の実装のように、ストアがアクションを受け取るたびに、ストアからステートオブジェクトが発行されることになります。 そして、このステートオブジェクトは再びマッピング関数を通過し、UIを更新します。ただし、マッピング関数の結果が前回から変化していない場合は、UIを再び更新する必要がないことに注意してください。 たとえば、
map(state => !!state.user)
の結果が前回の実行から変化していなければ、その結果を再びUI/Subscriberにプッシュする必要はありません。 これを実現するために、NgRx(RxJSではない)はselect
という特殊な演算子を導入しています。select
演算子を使用すると、上記のコードは次のように変わります。const isLoggedIn$ = this.store.pipe(select(state => !!state.user));
select
演算子は、マッピング関数の結果が前回から変化していない場合、UI/サブスクライバーに値がプッシュされないようにします。このアプローチはさらに改善することが可能です。
select
演算子が変更されていない値を UI/購読者にプッシュしない場合でも、毎回ステート オブジェクトを取得して結果を導き出すために計算を行う必要があります。すでに上で説明したように、ストアがアプリケーションからアクションを受信すると、
state
が observable によって発行されます。 アクションは常に状態を更新するわけではありません。 状態が変化していなければ、マッピング関数の計算結果も変化しません。 したがって、放出されたstate
オブジェクトが前回から変化していなければ、再度計算を行う必要はないのです。 ここでセレクタが登場します。セレクタは純粋な関数で、前回の実行結果を記憶しています。 入力が変更されていない限り、出力が再計算されることはありません。 その代わり、出力はメモリから返されます。 このプロセスはメモ化と呼ばれる。
NgRx はメモ化機能を持つセレクタを構築するために
createSelector
というユーティリティ関数を提供する。 以下は、createSelector
ユーティリティ関数の例です。createSelector
関数は、状態の異なるスライスを与える 1 対多のマッピング関数と、計算を実行するプロジェクター関数で取り込まれます。 プロジェクター関数は、状態のスライスが最後の実行から変更されていない場合は呼び出されません。 作成されたセレクタ関数を使用するには、select
演算子の引数として渡す必要があります。