Search code examples
angularrxjsangular-routingangular-httpclientrxjs-observables

Angular HttpClient observables fire repeatedly


The setup is as follows. Somebody calls my angular application with a url that includes an id to display the properties of the object with that id. Just like an online shop.

http://myFrontend/productDetails/10

I then use ActivatedRoute to get the id (10) and pass it on the HttpClient to fetch the requested data from the backend.

this.route.params.pipe(map(params => params.id)).subscribe(id => {
   const params = new HttpParams().set('id', id);
   this.httpClient.get<Product>('http://myBackend/product/', {params})
      .subscribe(product => this.product = product);
});

The problem gets visible when a user navigates, from this route to the same route for a diffrent product. so lets say You are at http://myFrontend/productDetails/10. There is an Observable that waits for the backend to return the product with the id 10 and store it into a component variable. Then you click next to navigate to http://myFrontend/productDetails/11. A new Observable is created that waits for the product with the id 11 to store it into the same component variable.

I don't really get why, but the first Observable that returns product 10 fires again, when I want to fetch product 11. Including a repeated http-request to http://myBackend/product/?id=10. Which in turn causes a race-condition because bothe of the the Subscriptions write their result the the same component variable.

I could stop that from happening by unsbscribing immediately after the observable returned for the first time or by piping it through take(1) but i'd like to understand why it is happening.


Solution

  • this.route.params fires every time the params changes. Which means it will create a new subscription every time the parameter changes. As you have in your example, it is generally an antipattern to have nested subscriptions. If you want to chain observables, you can use the so-called higher-order mapping operators, e.g., concatMap, mergeMap, switchMap, or exhaustMap. Another thing that you can do is not to subscribe explicitly by calling subscribe, but instead, you could utilize the async pipe directly in the template. This will unsubscribe automatically, which can prevent memory leaks (it's not a problem in your case.) Try this instead:

    this.route.params.pipe(concatMap(params => {
       const p = new HttpParams().set('id', params.id);
       return this.httpClient.get<Product>('http://myBackend/product/', {p})       
    }).subscribe(product => this.product = product);
    

    or

    let product$ = this.route.params.pipe(concatMap(params => {
           const p = new HttpParams().set('id', params.id);
           return this.httpClient.get<Product>('http://myBackend/product/', {p})       
        })
    
    ...in the template
    
    <div *ngIf="product$ | async as product">....</div>