Wouter Vandenputte
Wouter Vandenputte

Reputation: 125

Method in switchMap not being called again

I have a simple subject in MyComponent:

private readonly selectedID = new Subject<number | null>();

that gets triggered through HTML input from another component:

@Input()
public set myID(value: number | null)
{
    this.selectedID.next(value);
}

Every time a new value is given in it, I want to invoke an HTTP call defined in MyService and display that object. To achieve this, I created a second observable that should map the ID to a new observable of an HTTP call:

public readonly myModel$: Observable<IMyObject>;
public isLoading = false;

constructor(private myHttpService: MyHttpService, private myAlertService: MyAlertService)
{
    this.myModel$ = this.selectedID.pipe(
        tap(() => this.isLoading = true),
        switchMap(id => id == null ? of(null) : this.myService.getMyModel(id)),
        catchError(e =>
        {
            this.myAlertService.handleError(e);
            return of(null);
        }),
        tap(() => this.isLoading = false));
}

With this myModel$ observable defined, I pass it through the HTML template in an async-pipe that does the subscribing.

<ng-container *ngIf="myModel$ | async as m;">
  {{ m | json }}
</ng-container>
<ng-container *ngIf="isLoading">
    <i class="fas fa-spinner fa-spin" aria-hidden="true"></i>
</ng-container>

But the problem now is, the first time when my set myID is called, it simply works (server throws an error and alertService handles it, but that's not part of my problem). But when a new ID is given, myService.getMyModel is not being called again which it should.

I tried different RxJS operators, also using the decision tree but to no avail. I think something else is wrong than a mere higher order operator but I cannot figure out exactly what.

I also logged the execution of the first tap:

tap((value) => { this.isLoading = true; console.log('hit', value); ),

This also gets called only once, i.e. for every new ID given, whether it be a real number or null, it simply does not get logged any more after the first time.

Upvotes: 1

Views: 409

Answers (1)

olivarra1
olivarra1

Reputation: 3399

(server throws an error and alertService handles it, but that's not part of my problem)

That's exactly your problem :)

catchError doesn't magically restore the observable. The parent observable has thrown an error, it dies, and catchError lets you stop the propagation of that error, handle it how you want and return another observable to be consumed by the ones below.

Your catchError returns of(null), which means observers will receive null followed by [complete]. The subscription is closed and nothing else will happen.

You have an easy solution though: Move catchError inside the switchMap. This way, the observable that completes will be the one inside switchMap, and the outer subscription won't close:

constructor(private myHttpService: MyHttpService, private myAlertService: MyAlertService)
{
    this.myModel$ = this.selectedID.pipe(
        tap(() => this.isLoading = true),
        switchMap(id => id == null ? of(null) : this.myService.getMyModel(id).pipe(
            catchError(e => {
                this.myAlertService.handleError(e);
                return of(null);
            })
        )),
        tap(() => this.isLoading = false));
}

Upvotes: 1

Related Questions