object Object
object Object

Reputation: 305

NgRx - Multiple actions are being dispatch during the router transition

I've implemented the route resolver intended to request some data before navigating to the new route. Here is the piece of code responsible for dispatching the action to the store:

@Injectable()
export class MyResolver implements Resolve<any> {

  constructor(private store: Store<AppState>) {}

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any> {
    return this.store
      .pipe(
        tap(() => {
          this.store.dispatch(loadSomeData());
        }),
        first()
      );
  }
}

After hitting appropriate route and trigerring the resolver, I can see from Redux DevTools that my action has been dispatched twice before the navigation occured.

enter image description here

Well, my assumption then was that this is because of ROUTER_REQUEST and ROUTER_NAVIGATION being dispatched during the same router transition that causes the store to emit twice, in response to each dispatched action accordingly. However, if I comment out the line which dispatches the action and put there console.log instead, like so:

tap(() => {
    // this.store.dispatch(loadSomeData());
    console.log('Dispatching action');
  })

Router Store will still fire ROUTER_REQUEST and ROUTER_NAVIGATION actions but this time around the store will emit only once, as well as the test message is going to be outputted to the console just the one time.

How does the action dispatch make the difference?

Doesn't ROUTER_REQUEST and ROUTER_NAVIGATION actions matter in this situation?

Here is the StackBlitz you can run.

Upvotes: 3

Views: 1864

Answers (1)

Andrei Gătej
Andrei Gătej

Reputation: 11934

Apparently, there's no 'culprit' here.

This behavior is the result of how NgRx is using RxJS' entities.

The Store entity is an observable whose source is set to state$(the State entity - where data is stored):

export class Store<T = object> extends Observable<T>
  implements Observer<Action> {
  constructor(
    state$: StateObservable,
    private actionsObserver: ActionsSubject,
    private reducerManager: ReducerManager
  ) {
    super();

    this.source = state$;
  }
}

Source

This implies that every subscriber that subscribes to store(e.g: lazy.resolver - this.store.pipe(...)), will actually subscribe to State, which is a BehaviorSubject, meaning that the subscriber will be part of the subscribers list maintained by this subject.

As you know, a BehaviorSubject sends the latest nexted value to the new subscriber. This is pointed out by the first time that messaged is logged.

The reason that message is logged twice is because whenever you dispatch an action, a couple of things take place inside State.
The action will be intercepted in state, and when it happens, all the reducers will be invoked with the current state and the current action, resulting into a new state which is immediately sent to its subscribers:

// Inside `State.ts`

// Actions stream
const actionsOnQueue$: Observable<Action> = actions$.pipe(
  observeOn(queueScheduler)
);

// Making sure everything happens after the reducers have been set up
const withLatestReducer$: Observable<
  [Action, ActionReducer<any, Action>]
> = actionsOnQueue$.pipe(withLatestFrom(reducer$));

const seed: StateActionPair<T> = { state: initialState }; // Default state
const stateAndAction$: Observable<{
  state: any;
  action?: Action;
}> = withLatestReducer$.pipe(
  scan<[Action, ActionReducer<T, Action>], StateActionPair<T>>(
    reduceState, // Calling the reducers, resulting in a new state
    seed
  )
);

this.stateSubscription = stateAndAction$.subscribe(({ state, action }) => {
  this.next(state); // Send the new state to the subscribers(e.g in lazy.resolver)
  scannedActions.next(action); // Send the action so that it can be intercepted by the effects
});

Source

So when the store is initially subscribed, it will send its latest stored value, but when you dispatch another action(like) you do inside tap's next callback, a new state will be sent to the subscribers, which should explain why you're getting that messaged logged twice.

You might wonder how come we don't get into an infinite loop. These lines prevent that:

const actionsOnQueue$: Observable<Action> = actions$.pipe(
  observeOn(queueScheduler)
);

observeOn(queueScheduler) is very important here. Whenever an action is dispatched(e.g this.store.dispatch(loadSomeData());) it will not just pass along the action immediately(although it's using the queueScheduler), but it will schedule an action that will have to do some work(the work in this case is the task to pass along the action).

But when you do this.store.dispatch(loadSomeData());, it will reach the this.store.dispatch(loadSomeData()); again and before it reaches first().

I guess this is why is called queueScheduler; you're not getting an infinite loop error because the asyncScheduler(from which queueScheduler inherits) will make sure that if the scheduler is active(an action is doing its work, for example the first this.store.dispatch(loadSomeData())), the newly arrived action will be added to a queue, and will be taken into account when the active action finishes its work:

const { actions } = this;

if (this.active) {
  actions.push(action);
  return;
}

let error: any;
this.active = true;

do {
  if (error = action.execute(action.state, action.delay)) {
    break;
  }
} while (action = actions.shift()!); // exhaust the scheduler queue

Source

And because you're using first() and take(5), you're making sure than after an action finishes its work(e.g pushing the value further into the stream), the next action in queue won't have any effect, because, in this case, first() or take(5) would be reached before any actions in queue get the chance to perform their task.


If you'd like to read more about how ngrx/store works internally, I'd recommend having a look at Understanding the magic behind StoreModule of NgRx (@ngrx/store)

Upvotes: 3

Related Questions