web-dev-qa-db-ja.com

ngRx状態の更新とエフェクトの実行順序

私はこの質問について私自身の意見を持っていますが、再確認して確実に知っている方が良いです。注意を払って、助けようとしてくれてありがとう。ここにあります:

いくつかの状態変更をトリガーし、それにいくつかのエフェクトがアタッチされているアクションをディスパッチしていると想像してください。したがって、コードでは2つのことを行う必要があります-状態を変更し、いくつかの副作用を実行します。しかし、これらのタスクの順序は何ですか?それらを同期して行っていますか?最初に状態を変更してから副作用を実行すると思いますが、これら2つのタスクの間に別のことが発生する可能性はありますか?このように:状態を変更し、前に行ったHTTPリクエストで応答を取得して処理し、副作用を実行します。

[編集:]ここにコードを追加することにしました。また、私はそれを大幅に簡略化しました。

状態:

export interface ApplicationState {
    loadingItemId: string;
    items: {[itemId: string]: ItemModel}
}

行動:

export class FetchItemAction implements  Action {
  readonly type = 'FETCH_ITEM';
  constructor(public payload: string) {}
}

export class FetchItemSuccessAction implements  Action {
  readonly type = 'FETCH_ITEM_SUCCESS';
  constructor(public payload: ItemModel) {}
}

減力剤:

export function reducer(state: ApplicationState, action: any) {
    const newState = _.cloneDeep(state);
    switch(action.type) {
        case 'FETCH_ITEM':
            newState.loadingItemId = action.payload;
            return newState;
        case 'FETCH_ITEM_SUCCESS':
            newState.items[newState.loadingItemId] = action.payload;
            newState.loadingItemId = null;
            return newState;
        default:
            return state;
    }
}

効果:

@Effect()
  FetchItemAction$: Observable<Action> = this.actions$
    .ofType('FETCH_ITEM')
    .switchMap((action: FetchItemAction) => this.httpService.fetchItem(action.payload))
    .map((item: ItemModel) => new FetchItemSuccessAction(item));

そして、これがFetchItemActionをディスパッチする方法です。

export class ItemComponent {
    item$: Observable<ItemModel>;
    itemId$: Observable<string>;

    constructor(private route: ActivatedRoute,
                private store: Store<ApplicationState>) {

        this.itemId$ = this.route.params.map(params => params.itemId);

        itemId$.subscribe(itemId => this.store.dispatch(new FetchItemAction(itemId)));

        this.item$ = this.store.select(state => state.items)
            .combineLatest(itemId$)
            .map(([items, itemId]: [{[itemId: string]: ItemModel}]) => items[itemId])
    }
}

望ましいシナリオ:

User clicks on itemUrl_1;
we store itemId_1 as loadingItemId;
make the request_1;
user clicks on itemUrl_2;
we store itemId_2 as loadingItemId;
switchMap operator in our effect cancells previous request_1 and makes request_2;
get the item_2 in response;
store it under key itemId_2 and make loadingItemId = null.

悪いシナリオ:

User clicks on itemUrl_1;
we store itemId_1 as loadingItemId;
make the request_1;
user clicks on itemUrl_2;
we store itemId_2 as loadingItemId;  
we receive the response_1 before we made the new request_2 but after loadingItemId changed;
we store the item_1 from the response_1 under the key itemId_2;
make loadingItemId = null;
only here our effect works and we make request_2;
get item_2 in the response_2;
try to store it under key null and get an error

つまり、問題は単に悪いシナリオが実際に発生するかどうかです

15

したがって、コードでは2つのことを行う必要があります-状態を変更し、いくつかの副作用を実行します。しかし、これらのタスクの順序は何ですか?それらを同期して行っていますか?

アクションAをディスパッチするとします。アクションAを処理するレデューサーがいくつかあります。これらは、StoreModule.provideStore()に渡されるオブジェクトで指定された順序で呼び出されます。次に、アクションAをリッスンする副作用が発生します。はい、同期しています。

最初に状態を変更してから副作用を実行すると思いますが、これら2つのタスクの間に別のことが発生する可能性はありますか?このように:状態を変更し、前に行ったHTTPリクエストで応答を取得して処理し、副作用を実行します。

私は昨年半ばからngrxを使用していますが、これが事実であることに気づいたことはありません。私が見つけたのは、アクションがディスパッチされるたびに、最初にリデューサーによって処理され、次に次のアクションが処理される前に副作用によって処理されるサイクル全体を通過するということです。

Redux(ngrxの進化元)はメインページで予測可能な状態コンテナーとして自分自身に請求するため、これは事実であると思います。予測できない非同期アクションの発生を許可すると、何も予測できなくなり、redux開発ツールはあまり役に立ちません。

編集#1

だから私はただテストをしました。アクション「LONG」を実行したところ、副作用により10秒かかる操作が実行されました。その間、州へのディスパッチを増やしながら、UIを使い続けることができました。最後に、「LONG」の効果が終了し、「LONG_COMPLETE」がディスパッチされました。私は減速機と副作用がトランザクションであることについて間違っていました。

enter image description here

とは言っても、すべての状態変化はまだトランザクションであるため、何が起こっているのかを予測するのはまだ簡単だと思います。そして、これは良いことです。長時間実行されるAPI呼び出しを待機している間、UIがブロックされたくないからです。

編集#2

だから私がこれを正しく理解しているなら、あなたの質問の核心はswitchMapと副作用についてです。基本的には、最初のリクエストをキャンセルするためにswitchMapで副作用を実行するレデューサーコードを実行しているときに応答が返されるかどうかを尋ねています。

私はこの質問に答えると思いますテストを思いつきました。私がセットアップしたテストは、2つのボタンを作成することでした。 1つはクイックと呼ばれ、もう1つはロングと呼ばれました。 Quickは「QUICK」をディスパッチし、Longは「LONG」をディスパッチします。 Quickをリッスンするレデューサーはすぐに完了します。 Longをリッスンするレデューサーは、完了するまでに10秒かかります。

QuickとLongの両方をリッスンする単一の副作用を設定しました。これは、「of」を使用してapi呼び出しをエミュレートするふりをして、オブザーバブルを最初から作成できるようにします。これにより、「。delayを使用して」5秒待機してから、「QUICK_LONG_COMPLETE」がディスパッチされます。

  @Effect()
    long$: Observable<Action> = this.actions$
    .ofType('QUICK', 'LONG')
    .map(toPayload)
    .switchMap(() => {
      return of('').delay(5000).mapTo(
        {
          type: 'QUICK_LONG_COMPLETE'
        }
      )
    });

テスト中にクイックボタンをクリックし、すぐに長いボタンをクリックしました。

これが起こったことです:

  • クリックされたクイックボタン
  • 「QUICK」が発送されます
  • 副作用は5秒で完了するオブザーバブルを開始します。
  • 長いボタンがクリックされた
  • 「LONG」が発送されます
  • 減速機のLONGの処理には10秒かかります。 5秒のマークで、副作用からの元のオブザーバブルは完了しますが、「QUICK_LONG_COMPLETE」をディスパッチしません。さらに5秒経ちます。
  • 「LONG」をリッスンする副作用は、最初の副作用をキャンセルするスイッチマップを実行します。
  • 5秒が経過し、「QUICK_LONG_COMPLETE」が送出されます。

enter image description here

したがって、switchMapはキャンセルし、あなたの悪いケースは決して起こらないはずです。

17
seescode