ngRx state update and Effects execution order

12,362

So our code has to do 2 things - change state and do some side effects. But what is the order of these tasks? Are we doing them synchronously?

Let's say we dispatch action A. We have a few reducers that handle action A. Those will get called in the order they are specified in the object that is passed to StoreModule.provideStore(). Then the side effect that listens to action A will fire next. Yes, it is synchronous.

I believe that first, we change state and then do the side effect, but is there a possibility, that between these two tasks might happen something else? Like this: we change state, then get some response on HTTP request we did previously and handle it, then do the side effects.

I've been using ngrx since middle of last year and I've never observed this to be the case. What I found is that every time an action is dispatched it goes through the whole cycle of first being handled by the reducers and then by the side effects before the next action is handled.

I think this has to be the case since redux (which ngrx evolved from) bills itself as a predictable state container on their main page. By allowing unpredictable async actions to occur you wouldn't be able to predict anything and the redux dev tools wouldn't be very useful.

Edited #1

So I just did a test. I ran an action 'LONG' and then the side effect would run an operation that takes 10 seconds. In the mean time I was able to continue using the UI while making more dispatches to the state. Finally the effect for 'LONG' finished and dispatched 'LONG_COMPLETE'. I was wrong about the reducers and side effect being a transaction.

enter image description here

That said I think it's still easy to predict what's going on because all state changes are still transactional. And this is a good thing because we don't want the UI to block while waiting for a long running api call.

Edited #2

So if I understand this correctly the core of your question is about switchMap and side effects. Basically you are asking what if the response comes back at the moment I am running the reducer code which will then run the side effect with switchMap to cancel the first request.

I came up with a test that I believe does answer this question. The test I setup was to create 2 buttons. One called Quick and one called Long. Quick will dispatch 'QUICK' and Long will dispatch 'LONG'. The reducer that listens to Quick will immediately complete. The reducer that listens to Long will take 10 seconds to complete.

I setup a single side effect that listens to both Quick and Long. This pretends to emulate an api call by using 'of' which let's me create an observable from scratch. This will then wait 5 seconds (using .delay) before dispatching 'QUICK_LONG_COMPLETE'.

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

During my test I clicked on the quick button and then immediately clicked the long button.

Here is what happened:

  • Quick button clicked
  • 'QUICK' is dispatched
  • Side effect starts an observable that will complete in 5 seconds.
  • Long button clicked
  • 'LONG' is dispatched
  • Reducer handling LONG takes 10 seconds. At the 5 second mark the original observable from the side effect completes but does not dispatch the 'QUICK_LONG_COMPLETE'. Another 5 seconds pass.
  • Side effect that listens to 'LONG' does a switchmap cancelling my first side effect.
  • 5 seconds pass and 'QUICK_LONG_COMPLETE' is dispatched.

enter image description here

Therefore switchMap does cancel and your bad case shouldn't ever happen.

Share:
12,362
Dmytro Garastovych
Author by

Dmytro Garastovych

Updated on June 06, 2022

Comments

  • Dmytro Garastovych
    Dmytro Garastovych almost 2 years

    I have my own opinion on this question, but it's better to double check and know for sure. Thanks for paying attention and trying to help. Here it is:

    Imagine that we're dispatching an action which triggers some state changes and also has some Effects attached to it. So our code has to do 2 things - change state and do some side effects. But what is the order of these tasks? Are we doing them synchronously? I believe that first, we change state and then do the side effect, but is there a possibility, that between these two tasks might happen something else? Like this: we change state, then get some response on HTTP request we did previously and handle it, then do the side effects.

    [edit:] I've decided to add some code here. And also I simplified it a lot.

    State:

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

    Actions:

    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) {}
    }
    

    Reducer:

    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:

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

    And this is how we dispatch 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])
        }
    }
    

    Desired scenario:

    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.
    

    Bad scenario:

    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
    

    So the question is simply if the bad scenario can actually happen or not?