Search code examples
angularngrx

Catching Errors while calling an API from a NgRx effect


I'm trying to catch an error on the effect of a specific action. Everything is working fine, whether when the call succeeds or fails. However, I think the code is very verbose, and could be improved on, specially:

  • The catchError part, I'm returning empty() but I bieleve should be returning the SavedReplyDeleteFailed action. When doing so the whole effects breaks and does not compile, not sure on whats the best practice for handling this.
  • I'm relying too much on the tap and dispatch instead of returning the actions from the map/mergeMap call backs. When I do so the effects compilation breaks as well.

How I can apply the above improvements?

    deleteSavedReply = createEffect(() => this.actions$.pipe(
        ofType(SavedRepliesActions.SavedReplyDeleteRequested),
        mergeMap( action => {
            this.store.dispatch( SavedRepliesActions.DeleteLoading({ isLoading: true }) );
            return this.savedRepliesService.deleteSavedReply(action.id).pipe(
                tap(res => {
                    this.store.dispatch( SavedRepliesActions.SavedReplyDeleteSucess({ id: action.id }));
                    this.toastr.success( 'Saved Reply has been successfully deleted');
                }),
                catchError((error) => {
                    this.store.dispatch( SavedRepliesActions.SavedReplyDeleteFailed({message: error.message}) );
                    this.toastr.error( error.message, 'Something went wrong');
                    this.store.dispatch( SavedRepliesActions.DeleteLoading({ isLoading: false }) );
                    return empty();
                })
            );
        }),
        map(() => {
            return SavedRepliesActions.DeleteLoading({ isLoading: false });
        }),
    ));


Solution

  • You can opt for a more reactive way (using RxJS powerful), and so simplify a little your code with something like this:

    deleteSavedReply$ = createEffect(() => this.actions$.pipe(
      ofType(SavedRepliesActions.SavedReplyDeleteRequested),
      mergeMap(action => this.savedRepliesService.deleteSavedReply(action.id).pipe(
        switchMap(res => [
          SavedRepliesActions.SavedReplyDeleteSucess({ id: res.id }),
          UiActions.showToastrSuccess('Saved Reply has been successfully deleted')
        ]),
        catchError(error => of(
          SavedRepliesActions.SavedReplyDeleteFailed({ message: error.message }),
          UiActions.showToastrError(error.message, 'Something went wrong')
        ]))
      ))
    ))
    

    To do this, in the redux mindset, you should :

    • Create 2 new actions UiActions.showToastrSuccess and UiActions.showToastrError to show a message Toastr. Displaying a message is a could target for an action.

    • Use reducer to your loading flag. It's a slice of your state, so should change its value inside a reducer in place of the effect.

    For example :

    on(SavedRepliesActions.SavedReplyDeleteFailed, (state) => ({
      ...state,
      loading: false
    })),
    
    on(SavedRepliesActions.SavedReplyDeleteRequested, (state) => ({
      ...state,
      loading: true
    })),
    

    Other option

    Note that another option (or improvment...) could be to dispatch Toastr actions, inside SavedReplyDeleteSucess action. For example :

    deleteSavedReply$ = createEffect(() => this.actions$.pipe(
      ofType(SavedRepliesActions.SavedReplyDeleteRequested),
      mergeMap(action => this.savedRepliesService.deleteSavedReply(action.id).pipe(
        map(res => SavedRepliesActions.SavedReplyDeleteSucess({ id: res.id }))
        catchError(error => of(SavedRepliesActions.SavedReplyDeleteFailed({ message: error.message })))
      ))
    ))
    
    savedReplyDeleteSucess$ = createEffect(() => this.actions$.pipe(
      ofType(SavedRepliesActions.SavedReplyDeleteSucess),
      map(_ => UiActions.showToastrSuccess('Saved Reply has been successfully deleted'))
    ))
    
    savedReplyDeleteFailed$ = createEffect(() => this.actions$.pipe(
      ofType(SavedRepliesActions.SavedReplyDeleteFailed),
      map(message => UiActions.showToastrError(error.message, 'Something went wrong'))
    ))
    

    The advantage here is that you can make your code evolving, by adding some extra effects in case of success or failure. for example:

    savedReplyDeleteFailed$ = createEffect(() => this.actions$.pipe(
      ofType(SavedRepliesActions.SavedReplyDeleteFailed),
      switchMap(message => [
        UiActions.showToastrError(error.message, 'Something went wrong')),
        SystemActions.sendTraceError(error)
    ))
    

    Hope it will help.