NewToAngular
NewToAngular

Reputation: 1115

How to use RxJS to cancel previous HTTP Requests in an Angular Service

I know there are lots of questions like this but they have often confused me or have been difficult for me to apply to my situation, so I am asking here so I can get it round my head. In my component typescript file I have a method that calls a service that returns api data using http, so when a user clicks a chip on the interface they call the following..

fetchCounts(filters?: any): void {
    this.loaded = false;
    this.apiCall = this.api.getCounts(filters).subscribe((results: any) => {
        // do something with the returned data
        this.loaded = true;
        this.apiCall.unsunscribe()
    });
}

My api service looks like this:

getCounts(filters?: any): Observable<any> {
    let params: any;
    if (filters?.car?.length || filters?.bike?.length) {
      params = this.addObjectToParams(new HttpParams(), filters);
    }
    return this.http.get('api/counts', { params } )
      .pipe(map(this.extractData));
}

Now I have noticed when a user clicks my interface adding and removing chips that make an API call the interface seems to no longer show the true data due to the overload/large amounts of API calls. So I want to cancel the current api/http call if a new one is made (if fetchCounts is called again). I have tried adding a debounce to the .pipe in the getCountsmethod like so... .pipe( debounceTime(500), map(this.extractData)); but it doesn't seem to do anything. I think I have to add a switchMap but when I add my code it just breaks due to a lack of understanding on my part. I am currently looking through the docs... but any help would be appreciated.

Upvotes: 5

Views: 12708

Answers (2)

Mrk Sef
Mrk Sef

Reputation: 8022

If you want to avoid dealing with subscriptions, you can have a Subject manage all of that for you. That could look something like:

private _fetchCountsExe: Subject<any>;

constructor(){
  this._fetchCountsExe = new Subject<any>();
  this._fetchCountsExe.pipe(
    tap(_ => this.loaded = false),
    switchMap(filters => this.api.getCounts(filters))
  ).subscribe(results => {
    // do something with the returned data
    this.loaded = true;
  });
}

fetchCounts(filters?: any): void {
    this._fetchCountsExe.next(filters);
}

The code in constructor() can be put anywhere that runs as your service is initialized. Now switchMap will do all the work of canceling old subscriptions and switching into new ones.

This has many benefits.

Chief among them is that this design doesn't rely on javascript's event loop to work. Consider this simple example of unsubscribing from an observable:

const subscription = from([1,2,3,4]).subscribe(_ => 
  subscription.unsubscribe();
);

This will always fail. Because this observable is run synchronously, subscription will still be undefined and cannot be used to unsubscribe().

Your current code has a hidden race condition that turns out to work for you based on how Javascript's event loop works. This can, however, make your code brittle. For example, if you decide (in the future) to cache recent values and return them immediately if they're fresh enough, your code will unexpectedly break.

This approach also avoids putting a subscription in the global scope that's only used in one function. Instead, you have a Subject that describes the action and a function that initializes the action.

Upvotes: 1

Jasdeep Singh
Jasdeep Singh

Reputation: 8301

In this case, you can simply unsubscribe the previous call using unsubscribe method. switchMap is used when we have a higher-order observable.

Just unsubscribe the previous call before making the next one.

fetchCounts(filters?: any): void {
    this.loaded = false;
    this.apiCall && this.apiCall.unsunscribe(); // Unsubscribe here as well.
    this.apiCall = this.api.getCounts(filters).subscribe((results: any) => {
        // do something with the returned data
        this.loaded = true;
        this.apiCall.unsunscribe();
    });
}

Upvotes: 9

Related Questions