NgRx effects stream subscription

NgRx/effects is used to handle side effects in ngrx/store. It listens for actions that are dispatched in an observe stream and then return new action(s). Often we forget that a selector inside an observable stream still needs to be unsubscribed. We explain why in the following example.

In this example, there is a form on the page to register a user. Form has name and address fields. Once a save button is clicked, a save action dispatched, the user is saved and move onto next page. In effects, when a save user action captured, a service call made with the action params. The call returns the user Id. User id is then used to retrieve User object from store, and finally an array of action dispatched.

This is our effects code to handle save user action side effects.


@Effect()
public save$ = this.actions$
  .ofType(BuildActions.actionTypes.Save)
  .pipe(
    switchMap((action) => {
      const payload = {
        name: action.name,
        address: action.address,
      };
      return this.service.createOrUpdate(payload)
      .pipe(
        mergeMap(
          ((newId) => this.store.select(getUser(newId)))),
        switchMap((user) => {
          return [
            new UserPersistedAction(user),
            new NavAudienceAction(),
          ]
        })
      )
    }
  ));

The flow works fine. User would be saved and move onto next page. But when navigate back to the same page, all of a sudden, this switchMap block got triggered again:


switchMap((user) => {
  return [
    new UserPersistedAction(user),
    new NavAudienceAction(),
  ]
})

Nothing else in that effects got triggered, only this switchMap block. Examining with the Redux tool tells us that BuildActions.SaveAction was never fired again. So how could that switchMap block got triggered without the defined action fired?

It turns out that this.store.select(getUser(newId)) generates an observe stream and never got unsubscribed! So every time navigating back to the page, it'd continue its subscription by executing that switchMap block. It only stops when BuildActions.SaveAction fires off again and new stream generated. The fix is to add take(1), so once it's triggered once, it's done, behaving like being unsubscribed.

This is fixed code:


@Effect()
public save$ = this.actions$
  .ofType(BuildActions.actionTypes.Save)
  .pipe(
    switchMap((action) => {
      const payload = {
        name: action.name,
        id: action.id,
      };
      return this.service.createOrUpdate(payload)
      .pipe(
        mergeMap(
          ((newTacticId) =>   
           this.store.select(getTactic(newTacticId))
           .pipe(take(1)))
        ),
        switchMap((user) => {
          return [
            new UserPersistedAction(user),
            new NavAudienceAction(),
          ]
        })
      )
    }
  ));