HTTP - Optimize server interaction with debouncing
If you need to make an HTTP request in response to user input, it's not efficient to send a request for every keystroke. It's better to wait until the user stops typing and then send a request. This technique is known as debouncing.
Implement debouncing
Consider the following template, which lets a user enter a search term to find a package by name. When the user enters a name in a search-box, the PackageSearchComponent
sends a search request for a package with that name to the package search API.
<input type="text" (keyup)="search(getValue($event))" id="name" placeholder="Search"/>
<ul>
<li *ngFor="let package of packages$ | async">
<strong>{{package.name}} v.{{package.version}}</strong> -
<em>{{package.description}}</em>
</li>
</ul>
Here, the keyup
event binding sends every keystroke to the component's search()
method.
The type of $event.target
is only EventTarget
in the template.
In the getValue()
method, the target is cast to an HTMLInputElement
to let type-safe have access to its value
property.
getValue(event: Event): string {
return (event.target as HTMLInputElement).value;
}
The following snippet implements debouncing for this input using RxJS operators.
withRefresh = false;
packages$!: Observable<NpmPackageInfo[]>;
private searchText$ = new Subject<string>();
search(packageName: string) {
this.searchText$.next(packageName);
}
ngOnInit() {
this.packages$ = this.searchText$.pipe(
debounceTime(500),
distinctUntilChanged(),
switchMap(packageName =>
this.searchService.search(packageName, this.withRefresh))
);
}
constructor(private searchService: PackageSearchService) { }
The searchText$
is the sequence of search-box values coming from the user.
It's defined as an RxJS Subject
, which means it is a multicasting Observable
that can also emit values for itself by calling next(value)
, as happens in the search()
method.
Rather than forward every searchText
value directly to the injected PackageSearchService
, the code in ngOnInit()
pipes search values through three operators, so that a search value reaches the service only if it's a new value and the user stopped typing.
RxJS operators | Details |
---|---|
debounceTime(500) |
Wait for the user to stop typing, which is 1/2 second in this case. |
distinctUntilChanged() |
Wait until the search text changes. |
switchMap() |
Send the search request to the service. |
The code sets packages$
to this re-composed Observable
of search results.
The template subscribes to packages$
with the AsyncPipe and displays search results as they arrive.
See Using interceptors to request multiple values for more about the withRefresh
option.
Using the switchMap()
operator
The switchMap()
operator takes a function argument that returns an Observable
.
In the example, PackageSearchService.search
returns an Observable
, as other data service methods do.
If a previous search request is still in-flight, such as when the network connection is poor, the operator cancels that request and sends a new one.
NOTE:
switchMap()
returns service responses in their original request order, even if the server returns them out of order.
If you think you'll reuse this debouncing logic, consider moving it to a utility function or into the PackageSearchService
itself.