angulartypescriptrxjsrxjs-observablesrxjs-pipeable-operators

Angular RXJS optimize nested subscribes


Hey there I've an update function call with nested subscribe, interdependent, just that on the success of the parent call subscribe the inner one

updateSelectedScopes() {
    let params: any = {
     userInterface: this.userInterface,
     targetId: this.assessmentId,
     targetUserId: this.currentUser['userId'],
    };
    this.lockService.acquireLock(params).subscribe(lockingDetails => {
     if (lockingDetails) {
       let payload = this.consolidatedPayload
       this.assessmentService.updateSelectedScopes(payload).subscribe(res => {
         if (res) {
           this.releaseLock(
             this.assessmentId,
             this.currentUser['userId'],
             this.userInterface
           );
           this.modalService.dismissAll();
           if ((this.assessmentStatus === this.appConstants.CERTIFIED ||
             this.assessmentStatus === this.appConstants.UN_CERTIFIED ) ||
             this.assessmentStatus === this.appConstants.ASSESSMENT_FLAGGED) {
             this.assessmentService.initiateWorkflow(this.assessmentId).pipe(take(1)).subscribe(() => {
                 this.router.navigate([this.appConstants.VIEW_ASSESSMENT_URI], { skipLocationChange: true, queryParams: { 'assessmentId': this.assessmentId,  assessment: this.assessmentDetailsObj['assessmentName'], 'updateFlag': true } });
             }, (error) => {
               this.modalService.dismissAll();
               this.openAlert(error.error);
             });
         } else {
             this.router.navigate([this.appConstants.VIEW_ASSESSMENT_URI], { skipLocationChange: true, queryParams: { 'assessmentId': this.assessmentId, assessment: this.assessmentDetailsObj['assessmentName'], 'updateFlag': true } });
         }
         }
       }, error => {
         this.modalService.dismissAll();
         this.releaseLock(
           this.assessmentId,
           this.currentUser['userId'],
           this.userInterface
         );
         this.openAlert(error.error);
       });
     }
   }, error => {
     this.openLockDialog(
       error.error,
       this.currentUser['userId'],
       this.assessmentId,
     );
   })
 }

Can I optimize this function(with nested subscribes) with switchMap/mergeMap/flatMap, in order to avoid this ugly nesting subscribes. Any help would be great


Solution

  • Ususally in similar situation you should concatenate a series of Observables using the concatMap operator.

    concatMap ensures that as soon as the upstream Observable notifies something, the following Observable is "executed".

    In your logic it seems there is also some "non reactive" logic, specifically in the case of errors. To manage this situation you can use the catchError operator.

    The idea is the following

    // you have 3 functions that return observables
    const doStuff: (param: string) => Observable<string>;
    const doSomethingElse: (param: string) => Observable<string>;
    const doSomeMoreStuff: (param: string) => Observable<string>;
    
    doStuff("a param").pipe(
        concatMap((result) => {
            return doSomethingElse(result).pipe(
                catchError((error) => {
                    // do something with error that may occur in doSomethingElse
                    return of("")
                })
            )
        }),
        concatMap((result) => {
            return doSomeMoreStuff(result).pipe(
                catchError((error) => {
                    // do something with error that may occur in doSomeMoreStuff
                    return of("")
                })
            )
        }),
        catchError((error) => {
            // do something with error that may occur in doStuff
            return of("")
        })
    )
    .subscribe()
    

    In your case, your code could be refactored more or less like this (I do not have a working example and the code is a bit complex, so I could not test the following snippet and therefore there may be errors - it is meant just to give an idea)

    this.lockService.acquireLock(params).pipe(
        concatMap(lockingDetails => {
            if (lockingDetails) {
                let payload = this.consolidatedPayload
                return this.assessmentService.updateSelectedScopes(payload).pipe(
                    catchError(
                        (error) => {
                            this.modalService.dismissAll();
                            this.releaseLock(
                                this.assessmentId,
                                this.currentUser['userId'],
                                this.userInterface
                            );
                            this.openAlert(error.error);
                            return of(null)
                        }
                    )
                )  
            }
            return of(null)
        }),
        concatMap(res => {
            return this.assessmentService.updateSelectedScopes(payload).pipe(
                catchError(
                    (error) => {
                        this.modalService.dismissAll();
                        this.openAlert(error.error);
                        return of(null)
                    }
                )
            )
        }),
        catchError(
            (error) => {
                this.openLockDialog(
                    error.error,
                    this.currentUser['userId'],
                    this.assessmentId,
                );
            }
        )
     )
     .subscribe()
    

    This article can give you some inspiration for similar use cases.