機能の最も単純な実装は、最終的には良いものよりも多くの問題を引き起こし、他の場所では複雑さが増すこともあります。 最終結果は、誰も触れたくないザクザクのアーキテクチャです。
この記事は2017年に作成されましたが、この日に関連しています。 RxJSおよびNgrxの経験者、またはAngularでReduxを試してみたい人を対象としています。
コードスニペットは、現在のRxJS構文に基づいて更新され、読みやすさと理解しやすさを向上させるためにわずかに変更されました。
Ngrx / storeは、個々の機能の複雑さを含めるのに役立つAngularライブラリです。 理由の1つは、ngrx / storeが関数型プログラミングを採用しているため、関数内で実行できることを制限して、関数外でより合理的にすることです。 ngrx / storeでは、リデューサー(以下、レデューサーと呼びます)、セレクター(以下、セレクターと呼びます)、およびRxJS演算子のようなものは純粋な関数です。
純粋な関数は、テスト、デバッグ、分析、並列化、結合が簡単です。 次の場合、関数は純粋です。
- 同じ入力で、常に同じ出力を返します。
- 副作用なし。
副作用は避けることはできませんが、それらはngrx / storeで分離されているため、アプリケーションの残りの部分は純粋な関数で構成されている場合があります。
副作用
ユーザーがフォームを送信したら、サーバーで変更を加える必要があります。 サーバーの変更とクライアントへの応答は副作用です。 これはコンポーネントで処理できます。
this.store.dispatch({ type: 'SAVE_DATA', payload: data, }); this.saveData(data) // POST .pipe(map(res => this.store.dispatch({ type: 'DATA_SAVED' }))) .subscribe();
ユーザーがフォームを送信し、別の場所で副作用を処理するときに、コンポーネント内でアクション(以降、アクション)をディスパッチすることができれば便利です。
Ngrx / effectsは、ngrx / storeで副作用を処理するためのミドルウェアです。 監視可能なスレッドで送信されたアクションをリッスンし、副作用を実行し、新しいアクションをすぐにまたは非同期で返します。 返されたアクションは、reducerに渡されます。
RxJSの方法で副作用を処理する機能により、コードがよりクリーンになります。 コンポーネントから初期アクションSAVE_DATA
送信した後、残りを処理するエフェクトクラスを作成します。
@Effect() saveData$ = this.actions$.pipe( ofType('SAVE_DATA'), pluck('payload'), switchMap(data => this.saveData(data)), map(res => ({ type: 'DATA_SAVED' })), );
これは、アクションを送信してobservableにサブスクライブする前にのみ、コンポーネントの操作を簡素化します。
Ngrx /エフェクトを乱用しやすい
Ngrx /エフェクトは非常に強力なソリューションであるため、悪用しやすいです。 以下は、Ngrx /エフェクトによって簡素化される一般的なngrx /ストアアンチパターンです。
1.重複状態
何らかのマルチメディアアプリケーションで作業しており、状態ツリーに次のプロパティがあるとします。
export interface State { mediaPlaying: boolean; audioPlaying: boolean; videoPlaying: boolean; }
オーディオはメディアの一種であるため、 audioPlaying
がtrueの場合はいつでも、 mediaPlaying
もtrueでmediaPlaying
必要があります。 そこで質問があります:「audioPlayingが更新されたときにmediaPlayingが更新されたことを確認するにはどうすればよいですか?」
無効な答え :Ngrx /エフェクトを使用してください!
@Effect() playMediaWithAudio$ = this.actions$.pipe( ofType('PLAY_AUDIO'), map(() => ({ type: 'PLAY_MEDIA' })), );
正しい答え : mediaPlaying
の状態が状態ツリーの別の部分によって完全に予測される場合、これは本当の状態ではありません。 これは派生状態です。 ストアではなくセレクターに属します。
audioPlaying$ = this.store.select('audioPlaying'); videoPlaying$ = this.store.select('videoPlaying'); mediaPlaying$ = combineLatest(this.audioPlaying$, this.videoPlaying$).pipe( map(([audioPlaying, videoPlaying]) => audioPlaying || videoPlaying), );
これで、条件はクリーンで正規化されたままになり、副作用ではない何かにNgrx /効果を使用しなくなります。
2.レデューサーを使用したアクションの連鎖
状態ツリーに次のプロパティがあると想像してください。
export interface State { items: { [index: number]: Item }; favoriteItems: number[]; }
その後、ユーザーはアイテムを削除します。 削除要求が返されると、 DELETE_ITEM_SUCCESS
アクションDELETE_ITEM_SUCCESS
送信され、アプリケーションの状態が更新されます。 items
レデューサーでは、個々のItem
がitems
オブジェクトから削除されます。 ただし、この要素識別子がfavoriteItems
配列にある場合、それが参照する要素は存在しません。 質問は、 DELETE_ITEM_SUCCESS
アクションを送信するときに、 DELETE_ITEM_SUCCESS
から識別子が確実に削除されるようにする方法です。
無効な答え :Ngrx /エフェクトを使用してください!
@Effect() removeFavoriteItemId$ = this.actions$.pipe( ofType('DELETE_ITEM_SUCCESS'), map(() => ({ type: 'REMOVE_FAVORITE_ITEM_ID' })), );
そのため、2つのアクションが次々に送信され、2つのレデューサーが新しい状態を次々に返します。
正解 : DELETE_ITEM_SUCCESS
は、 items
レデューサーとfavoriteItems
レデューサーの両方で処理できます。
export function favoriteItemsReducer(state = initialState, action: Action) { switch (action.type) { case 'REMOVE_FAVORITE_ITEM': case 'DELETE_ITEM_SUCCESS': const itemId = action.payload; return state.filter(id => id !== itemId); default: return state; } }
アクションの目的は、何が起こったのかを、状態がどのように変化するべきかから分離することです。 起こったのはDELETE_ITEM_SUCCESS
です。 レデューサーのタスクは、対応する状態の変化を引き起こすことです。
favoriteItems
から識別子を削除しても、 Item
を削除しても副作用favoriteItems
ません。 プロセス全体が完全に同期され、レデューサーによって処理できます。 Ngrx /エフェクトは必要ありません。
3.コンポーネントのデータを要求する
コンポーネントにはストアからのデータが必要ですが、最初にサーバーから取得する必要があります。 問題は、コンポーネントがデータを受信できるように、データをストアにどのように配置できるかです。
痛みを伴う方法 :Ngrx /エフェクトを使用してください!
コンポーネントでは、アクションを送信してリクエストを開始します。
ngOnInit() { this.store.dispatch({ type: 'GET_USERS' }); }
エフェクトクラスでは、 GET_USERS
をリッスンしGET_USERS
。
@Effect getUsers$ = this.actions$.pipe( ofType('GET_USERS'), withLatestFrom(this.userSelectors.needUsers$), filter(([action, needUsers]) => needUsers), switchMap(() => this.getUsers()), map(users => ({ type: 'RECEIVE_USERS', users })), );
ここで、ユーザーが特定のルートの読み込みに時間がかかりすぎると判断したため、あるルートから別のルートに切り替えたとします。 効率的で不要なデータをロードしないために、このリクエストをキャンセルします。 コンポーネントが破棄されると、アクションを送信してリクエストの購読を解除します。
ngOnDestroy() { this.store.dispatch({ type: 'CANCEL_GET_USERS' }); }
エフェクトクラスでは、両方のアクションをリッスンします。
@Effect getUsers$ = this.actions$.pipe( ofType('GET_USERS', 'CANCEL_GET_USERS'), withLatestFrom(this.userSelectors.needUsers$), filter(([action, needUsers]) => needUsers), map(([action, needUsers]) => action), switchMap( action => action.type === 'CANCEL_GET_USERS' ? of() : this.getUsers().pipe(map(users => ({ type: 'RECEIVE_USERS', users }))), ), );
いいね 次に、別の開発者が同じHTTP要求を必要とするコンポーネントを追加します(他のコンポーネントについては想定していません)。 コンポーネントは同じ場所で同じアクションを送信します。 両方のコンポーネントが同時にアクティブである場合、最初のコンポーネントはHTTP要求を開始して初期化します。 2番目のコンポーネントが初期化されると、 needUsers
がfalse
なるため、余分なことは何も起こりません。 いいね!
次に、最初のコンポーネントが破棄されると、 CANCEL_GET_USERS
が送信されCANCEL_GET_USERS
。 ただし、2番目のコンポーネントにはまだこのデータが必要です。 リクエストがキャンセルされるのを防ぐにはどうすればよいですか? たぶん、すべてのサブスクライバーのカウンターを開始しますか? 私はこれを理解しませんが、あなたは、本質を理解したと思います。 これらのデータ依存関係を管理するより良い方法があると疑い始めています。
次に、別のコンポーネントが表示され、 users
データがストアに表示されるまで取得できないデータに依存しているとします。 これは、チャットWeb接続、一部のユーザーに関する追加情報などです。 このコンポーネントが初期化されるかどうかは、他の2つのコンポーネントをusers
サブスクライブする前または後に知りません。
この特定のシナリオで私が見つけた最高の助けは、 この素晴らしい投稿です。 彼の例では、 callApiY
はcallApiX
すでに完了している必要がcallApiX
。 コメントを削除して、威圧的にならないようにしましたが、元の投稿を読んで詳細を確認してください。
@Effect() actionX$ = this.actions$.pipe( ofType('ACTION_X'), map(toPayload), switchMap(payload => this.api.callApiX(payload).pipe( map(data => ({ type: 'ACTION_X_SUCCESS', payload: data })), catchError(err => of({ type: 'ACTION_X_FAIL', payload: err })), ), ), ); @Effect() actionY$ = this.actions$.pipe( ofType('ACTION_Y'), map(toPayload), withLatestFrom(this.store.select(state => state.someBoolean)), switchMap(([payload, someBoolean]) => { const callHttpY = v => { return this.api.callApiY(v).pipe( map(data => ({ type: 'ACTION_Y_SUCCESS', payload: data, })), catchError(err => of({ type: 'ACTION_Y_FAIL', payload: err, }), ), ); }; if (someBoolean) { return callHttpY(payload); } return of({ type: 'ACTION_X', payload }).merge( this.actions$.pipe( ofType('ACTION_X_SUCCESS', 'ACTION_X_FAIL'), first(), switchMap(action => { if (action.type === 'ACTION_X_FAIL') { return of({ type: 'ACTION_Y_FAIL', payload: 'Because ACTION_X failed.', }); } return callHttpY(payload); }), ), ); }), );
ここで、コンポーネントが不要になったときにHTTPリクエストをキャンセルする必要があるという要件を追加すると、これはさらに複雑になります。
。 。 。
では、RxJSで本当に簡単にできるのに、なぜデータ依存関係管理に多くの問題があるのでしょうか?
サーバーからのデータは技術的には副作用ですが、Ngrx /効果がこれを処理する最良の方法であるとは思えません。
コンポーネントはユーザー入力/出力インターフェイスです。 データを表示し、彼が実行したアクションを送信します。 コンポーネントがロードされると、このユーザーが実行したアクションは送信されません。 彼はデータを見せたい。 これは副作用というよりサブスクリプションに似ています。
多くの場合、アクションを使用してデータ要求を開始するアプリケーションを見ることができます。 これらのアプリケーションは、副作用を通して観察可能な特別なインターフェースを実装しています。 そして、私たちが見たように、このインターフェースは非常に不便で面倒になります。 Observable自体へのサブスクライブ、サブスクライブ解除、リンクは、はるかに簡単です。
。 。 。
苦痛の少ない方法 :コンポーネントは、observableを介してサブスクライブすることにより、データへの関心を登録します。
必要なHTTPリクエストを含むobservableを作成します。 エフェクトを介してこれを行うよりも、純粋なRxJSを使用して、相互に依存する複数のサブスクリプションとクエリチェーンを管理するのがどれほど簡単かがわかります。
サービスでこれらのオブザーバブルを作成します。
requireUsers$ = this.store.pipe( select(selectNeedUser), filter(needUsers => needUsers), tap(() => this.store.dispatch({ type: 'GET_USERS' })), switchMap(() => this.getUsers()), tap(users => this.store.dispatch({ type: 'RECEIVE_USERS', users })), finalize(() => this.store.dispatch({ type: 'CANCEL_GET_USERS' })), share(), ); users$ = muteFirst( this.requireUsers$.pipe(startWith(null)), this.store.pipe(select(selectUsers)), );
users$
へのサブスクリプションusers$
は、 requireUsers$
とthis.store.pipe(select(selectUsers))
両方に送信されthis.store.pipe(select(selectUsers))
が、データはthis.store.pipe(select(selectUsers))
からのみ受信されthis.store.pipe(select(selectUsers))
muteFirst
および固定muteFirst
実装例this.store.pipe(select(selectUsers))
彼女のテストで 。)
コンポーネント内:
ngOnInit() { this.users$ = this.userService.users$; }
このデータ依存関係は単純なオブザーバブルになったため、 async
パイプを使用してテンプレートでサブスクライブおよびサブスクライブ解除でき、アクションを送信する必要がなくなりました。 アプリケーションがデータ用に署名された最後のコンポーネントのルートを離れると、HTTP要求がキャンセルされるか、Webソケットが閉じられます。
データ依存関係チェーンは次のように処理できます。
requireUsers$ = this.store.pipe( select(selectNeedUser), filter(needUsers => needUsers), tap(() => this.store.dispatch({ type: 'GET_USERS' })), switchMap(() => this.getUsers()), tap(users => this.store.dispatch({ type: 'RECEIVE_USERS', users })), share(), ); users$ = muteFirst( this.requireUsers$.pipe(startWith(null)), this.store.pipe(select(selectUsers)), ); requireUsersExtraData$ = this.users$.pipe( withLatestFrom(this.store.pipe(select(selectNeedUsersExtraData))), filter(([users, needData]) => Boolean(users.length) && needData), tap(() => this.store.dispatch({ type: 'GET_USERS_EXTRA_DATA' })), switchMap(() => this.getUsers()), tap(users => this.store.dispatch({ type: 'RECEIVE_USERS_EXTRA_DATA', users, }), ), share(), ); public usersExtraData$ = muteFirst( this.requireUsersExtraData$.pipe(startWith(null)), this.store.pipe(select(selectUsersExtraData)), );
上記の方法とこの方法の並列比較を次に示します。
純粋なObservableを使用すると、必要なコード行が少なくなり、チェーン全体のデータ依存関係から自動的にサブスクライブ解除されます。 (元々含まれていたfinalize
ステートメントをスキップして比較を理解しやすくしましたが、それらがなくてもクエリはキャンセルされます。)
おわりに
Ngrx /エフェクトは素晴らしいツールです! ただし、使用する前に次の質問を検討してください。
- これは本当に副作用ですか?
- Ngrx /エフェクトはこれを行う最良の方法ですか?