Search code examples
angulartypescriptrxjsobservablengondestroy

Remove observable action is subscription is unsubscribed


I have a problem which I tought I can solve with a subscription:

refresh$: Subscription;
data$: Subscription;

ngOnInit() {
  this.refresh = interval(1000).subscribe(() => {
      this.getData();
    }
  );
}

ngOnDestroy() {
  this.refresh$.unsubscribe();
  this.data$.unsubscribe();
}

getData() {
  this.data$ = service.getData().subscribe(response => {
    // here, based on response, I update the header component value whithin an event
  }, err => {
    // also, if catch error, update header component
  }); 
}

Because I have an interval at 1 seconds and the server is down (intentional), my interval will emit 5 requests in 5 seconds, but the answer foreach will came in much time as 1 second.

So, when I emit first request and wait its answer (which will throw an error), already will emit the second request, the thirs, and so on.

In this time, if I leave the page (calling ngOnDestroy), I want to update the header from another component. But, after leaving the page, I will receive all the responses (success or failure) of the previous component. I want to cancell all these when I leave it. I thought that unsubscribing to data$ will solve this, but the problem persist.

thanks


Solution

  • You have nested subscriptions which is bad practice and makes it harder to unsubscribe from all inner subscriptions. Use an observable mapping operator like mergeMap, switchMap, concatMap or exhaustMap to map to an inner observable and use takeUntil to unsubscribe.

    private destroy$ = new Subject();
    
    ngOnInit() {
      interval(1000).pipe(
        concatMap(() => this.getData()),
        takeUntil(this.destroy$)
      ).subscribe(response => {
        // update the header component value based on response
      });
    }
    
    ngOnDestroy() {
      this.destroy$.next();
      this.destroy$.complete();
    }
    
    getData(): Observable<any> {
      return service.getData().pipe(
        catchError(error => {
          // update header component on error
          return EMPTY; // if the subscribe callback shouldn't be executed on errors
    
          // OR return an observable with the value the subscribe callback in ngOnInit should receive
          return of(/* what the header should be set to on errors */)
        })
      ); 
    }
    

    You could also use the async pipe to subscribe and handle the subscription. Maybe use timer instead of interval to send the first request without an initial delay.

    data$ = timer(0, 1000).pipe(
      concatMap(() => this.getData()),
    );
    
    getData(): Observable<any> {
      return service.getData().pipe(
        catchError(error => {
          return of(/* what the header should be set to on errors */)
        })
      ); 
    }
    
    <header>
      <ng-container *ngIf="data$ | async as data">
        {{ data }}
      </ng-container>
    </header>