2

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.

Liam
  • 27,717
  • 28
  • 128
  • 190
Jean D.
  • 169
  • 3
  • 15

2 Answers2

2

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;
        })
      );
  }
Picci
  • 16,775
  • 13
  • 70
  • 113
  • Thanks, currently observing ;) your idea. Found out: Due to a deprecation-warning for `forkJoin` I need to wrap both Observables in an array. – Jean D. Apr 24 '20 at 12:10
0

Here's how I'd do it:

const decorateObservable = (obs$, key) => obs$.pipe(
  mapTo(false), // `false` -> no error
  catchError(() => of(true)), // `true` -> error found
  map(value => ({ key, value })) // Identification
)

const base$ = merge(
  decorateObservable(obs1$, 'obs1'),
  decorateObservable(obs2$, 'obs2'),
).pipe(
  // When the source is unsubscribed(`error`/`complete`),
  finalize(() => this.isLoading = false),
  share(),
)

const obs1Displayed$ = base$.pipe(
  filter(o => o.key === 'obs1'),
  map(o => o.value),
)

const obs2Displayed$ = base$.pipe(
  filter(o => o.key === 'obs2'),
  map(o => o.value),
)

The share() operator is used here because subscribing multiple times to the source is not desired.

In this case, you'd be subscribing twice(in the template), because both displayed observables derive from the same base. What share does it to multicast the data producer.

share is the same as pipe(multicast(() => new Subject()), refCount()). refCount indicates that the producer will be invoked as soon as the first subscriber comes in.

Put differently, the data consumer sort of decides when the producer should start its logic.

Note that I've assumed that obs1$ and obs2$ are producing values asynchronously.

Essentially, it's almost the same as doing:

const s  = new Subject();

// The subscriptions happen inside the template
s.pipe(filter(o => o.key === 'obs1', ...).subscribe(observer1)
s.pipe(filter(o => o.key === 'obs2', ...).subscribe(observer2)

// And later on...
s.next({ key: 'obs1', value: false }) // `obs1$` emitted

And here's the template:

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

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

<ng-container *ngIf="obs2Displayed$ | async">
  <div class="error">
    Technical error
  </div>
  More content here which is shown when at least Obs1 doesn't had an error
  </div>
Andrei Gătej
  • 11,116
  • 1
  • 14
  • 31