Search code examples
angularrxjsobservableangular-observablerxjs-observables

Wait for two observables (incl. failed one) before rendering template


I have two Observables. The rendering of the template should only start when BOTH Observables are completed or failed:

  • Observable 1 completes and Observable 2 completes or
  • Observable 1 completes, but Observable 2 fails
  • When Observable 1 fails Observable 2 is not important because the template won't be rendered completely then

(Ignore the <any> type, it's only for simplification here)

Component:

@Component({
  selector: 'app-page',
  templateUrl: './page.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class PageComponent implements OnInit {
  obs1$ = new Subject<any>();
  obs2$ = new Subject<any>();

  isLoading = true;
  isObs1Error: boolean;
  isObs2Error: boolean;


  ngOnInit() {
    this.initializeDataRetrieval();
  }

  initializeDataRetrieval() {
    this.obs1$ = this.obs1Method();
    this.obs1$.subscribe((response: any) => {
      this.isObs1Error = false;
      this.obs1 = response;

      this.obs2$ = this.obs2Method();
      this.obs2$.subscribe((response: any) => {
        this.isObs2Error = false;
        this.isLoading = false;
        this.obs2 = response;
        this.cdr.detectChanges();
      });
    });
  }

  private obs1Method(): any {
    return this.obs1Service
      .getStuff()
      .pipe(
        catchError(() => {
          this.isError = true;
          this.isLoading = false;
          this.cdr.detectChanges();
          return EMPTY;
        })
      );
  }

  private obs2Method(): any {
    return this.obs2Service
      .getStuff()
      .pipe(
        catchError(() => {
          this.isObs2Error = true;
          this.isLoading = false;
          this.cdr.detectChanges();
          return EMPTY;
        })
      );
  }

  canDisplayContent(): boolean {
    return !this.isLoading && !this.isObs1Error;
  }

Template:

<ng-container *ngIf="isLoading">
  <app-loading-indicator></app-loading-indicator>
</ng-container>

<ng-container *ngIf="isObs1Error">
  <div class="error">
    This Obs1 stuff could not be loaded currently
  </div>
</ng-container>

<ng-container *ngIf="canDisplayContent()">
  <div class="error" *ngIf="isObs2Error">
    Technical error
  </div>
  More content here which is shown when at least Obs1 doesn't had an error
</div>

So basically:

  • I wanna wait with template rendering until both Observables are done and display a loading indicator during the time
  • When there is an error with Obs1 then show a message
  • When there is an error with Obs2 then render the 3rd ng-container with the Obs2 error message

I'm sure the TS code can be simplified by the usage of ... which RxJS operator? Although reading through RxJS Operators for Dummies: forkJoin, zip, combineLatest, withLatestFrom I'm not sure if any of these fits. As far as I understood e.g. combineLatest only succeeds when both streams complete successfully ...

Any hint is welcome, thanks.


Solution

  • I would consider to use forkJoin for this case.

    The code would look like this

    forkJoin(this.obs1Method(), this.obs2Method()).subscribe(
       ({resp1, resp2}) => {
            this.isLoading = false;
            this.obs2 = resp1;
            this.obs2 = resp2;
            this.cdr.detectChanges()
       }
    )
    

    You would have probably also to change slightly the obsxMethods adding a tap to set the error properties to false in case of successful retrieval of data and remove the settings which are performed within the subscribe, like this

    private obs1Method(): any {
        return this.obs1Service
          .getStuff()
          .pipe(
            tap(() => this.isError = false),
            catchError(() => {
              this.isError = true;
              return EMPTY;
            })
          );
      }
    
      private obs2Method(): any {
        return this.obs2Service
          .getStuff()
          .pipe(
            tap(() => this.isObs2Error = false),
            catchError(() => {
              this.isObs2Error = true;
              return EMPTY;
            })
          );
      }