Kevin Amorim
Kevin Amorim

Reputation: 535

Angular 9 - Avoiding multiple API calls on typeahead feature

I'm implementing a search box that should make a call to an API with the value the user entered. I want to make the call when the user stops typing for some ms.

This works fine for the first request:

this.searchForm.get('searchQuery').valueChanges.pipe(
  filter(data => data.trim().length > 0),
  debounceTime(500),
  switchMap( (query: string) => this.productsService.searchProducts(query))
).subscribe();

But, if I continue to type it waits for another 500ms and then sends multiple requests (equal to the number of characters typed, for what it seems).

productsChanged = new Subject<Product[]>();

  searchProducts(query: string) {
    return this.http.get<Product[]>(this.baseUrl + '/products/search/' + query)
        .pipe(
          tap(products => {
            this.products = products;
            this.productsChanged.next(this.products.slice());
          }));
   }

It will probably be something very simple, but I can't seem to understand what's happening.

Stackblitz: https://stackblitz.com/edit/angular-ivy-w4mbhm


Solution

I have found the problem.

I was listening to (ngModelChange):

  <input
name="searchQuery" 
type="text" 
[(ngModel)]="searchQuery"
(ngModelChange)="onSearch()"
formControlName="searchQuery" />

And inside that listener I was appending a new listener to 'valueChanges'. So on each key stroke, a new listener was created.

As expected, a simple mistake, that made me spend some hours.

Thanks for your help!

Upvotes: 0

Views: 1437

Answers (1)

Poul Kruijt
Poul Kruijt

Reputation: 71911

Like I suspected, you are subscribing constantly to your form changes. You should only do this once in your component (or not at all, if you use the async pipe):

working stack

ngOnInit(): void {
  this.searchForm = new FormGroup({
    searchQuery: new FormControl()
  });

  this.searchForm
    .get("searchQuery")
    .valueChanges.pipe(
      filter(data => data?.trim().length > 0),
      debounceTime(500),
      switchMap((query: string) => this.productsService.searchProducts(query).pipe(
        catchError((e) => of([])
      )))
    )
    .subscribe((e) => {
      console.log(e)
    });
}

You can also do:

ngOnInit(): void {
  this.products$ = this.searchForm.get("searchQuery").valueChanges.pipe(
    // ...
  )
}

and use the async pipe. This way you don't have to unsubscribe on destroy

Upvotes: 1

Related Questions