Search code examples
angularrxjsngrxngrx-effects

NgRx Effect - inner catchError executes multiple times


I have an Angular app with NgRx, inside one of my effects I have a non expected behavior that, turns out, is expected, but I don't know how to fix it.

My minified code is

@Effect()
detelete$ = this.actions$
  .pipe(
    ofType<fromActions.DeleteRequested>(fromActions.ActionTypes.DeleteRequested),
    map(action => action.payload.id),
    mergeMap(id =>
      combineLatest([
        of(id),
        this.rservice.deleteById(id)
          .pipe(
            catchError((err, caught$) => {
              this.store.dispatch(new fromActions.DeleteCancelled({id: id}));
              return caught$;
            })
          )
      ])
    ),
    map(([id, ]: [string, any]) => {
      return new fromActions.DeleteSuccess({id: id});
    }),
  );

The reason I don't have my catchError on the same level as my mergeMap is that I need my id as payload for my fromActions.DeleteCancelled action. Plus, my service only returns a boolean, so I use combineLatest to persist it to my onSuccess map.

What I'm experiencing is this catchError is executing multiple times. Thus, dispatching my error action multiple times.
I foun that

If you return this source, the observable will effectively restart all over again and retry

In this case the source is my caught$.

If, inside my cacthError I return

  return of(new fromActions.DeleteCancelled({id: id}));

It will still go to my onSuccess map. I could maybe check if my second param inside that map is os type Action or boolean, but I think there's a proper way to handle it and I don't know it.

StackBlitz (uncomment subscribe to see infinite loop)

Thanks.


Solution

  • You could consider this approach. Your code could looks like below:

    import { EMPTY, of, Observable } from 'rxjs';
    ...
    
    @Effect()
    delete$ = this.actions$.pipe(
      ofType<fromActions.DeleteRequested>(fromActions.ActionTypes.DeleteRequested),
      map(action => action.payload.id),
      switchMap((id) => this.rservice.deleteById(id).pipe(
        switchMap(result => {
          return result ? of(new fromActions.DeleteSuccess({id: id})) : EMPTY
        }),
        catchError(error => {
          return of(new fromActions.DeleteCancelled({id: id}))
        })
      )
    )
    

    if your service is returning true, so DeleteSuccess action is dispatched, else an empty observable that completes immediately. Because EMPTY returns an observable, you have to use switchMap, and also of to return an observable of action DeleteSuccess.

    In case of error, DeleteCancelled is dispatched.

    Another approach could be to use @Effect({ dispatch: false }), and manually dispatch all needed actions.

    Hope it will help.