Debouncing is the delay of a function/method execution or an action for a period of the specified time. During this specified time, calls to the method/function or action are collected and executes each one when the specified has elapsed.
Suppose you have a blog website with a search bar where users can search for posts. When a user types in the name of a blog to search in the search bar, an HTTP request is sent to the web API to search for blog post names or mentions with the search tokens.
You will find out that the server will always be called on every single letter typed. For example, you want articles based on "Javascript" and "optimization", then you typed "Javascript optimization". For every single letter in "Javascript optimization", the server will be called plus space, i.e the server will be called 23 times just to search for blog posts with "Javascript optimization" keywords and *ngFor will trigger change detection run on the component 10 times.
The code in Angular will look like this:
@Component({
...,
template: `
<input type="text" placeholder="Search" (keyup)="search(evt)"
<div class="search-result" *ngFor="let result of results$ | async">
//...
</div>
`
})
class SearchBarComponent {
results$: Observable
constructor(private httpClient: HttpClient) {}
search(evt) {
const searchText = evt.target.value
this.results$ = this.httpClient.get("/api/search?q=" + searchText)
}
}
See, we bound the keyup event to the input#text to call the search method whenever we type on the text input. The search method extracts the data typed in the input#textbox and performs an HTTP GET request to "/api/search/"
via HttpClient, the result is assigned to results$ and is displayed on the DOM using *ngFor
and async
pipe.
Sending HTTP request for every keystroke could become very expensive. We have to delay the HTTP call to the server for a considerable amount of time before triggering a search for the currently typed word. This is called debouncing, it waits for a specified time before calling a function.
Let's see the ways by which we can debounce the search method. First, is the use of the popular library RxJS operators: debounce and debounceTime
debounce
We will use RxJS debounce
operator to delay calls to the search method
debounce emits a value form the source Observable only after a particular life span determined by another Observable has passed without another source emission.
@Component({
...,
template: `
<input type="text" placeholder="Search" (keyup)="search(evt)"
<div class="search-result" *ngFor="let result of results$ | async">
//...
</div>
`
})
class SearchBarComponent implements OnInit {
results$: Observable
subject = new Subject()
constructor(private httpClient: HttpClient) {}
ngOnInit() {
this.results$ = this.subject.pipe(
debounce(() => Rx.Observable.interval(1000)),
map(searchText => this.httpClient.get("/api/search?q=" + searchText))
)
}
search(evt) {
const searchText = evt.target.value
// emits the `searchText` into the stream. This will cause the operators in its pipe function (defined in the ngOnInit method) to be run. `debounce` runs and then `map`. If the time interval of 1 sec in debounce hasn't elapsed, map will not be called, thereby saving the server from being called.
this.subject.next(searchText)
}
}
We added a Subject (a type of Observable), this is to form a gateway in which the text search will pass before hitting the HttpClient. We used the pipe function in the Subject to add operators: debounce and map.
We set the debounce
to be 1 sec (1000ms), debounce
depends on external source Observable to set the time debounce would have to delay emission, we used the interval function to set the time. The map
has a callback function that will perform the HTTP GET request to the server. With this, we have added a delay to our search mechanism.
debounceTime
We can write cleaner code with debounceTime, this operator doesn't need time to be set by a source Observable, here we pass the time delay to the debounceTime operator.
@Component({
...,
template: `
<input type="text" placeholder="Search" (keyup)="search(evt)"
<div class="search-result" *ngFor="let result of results$ | async">
//...
</div>
`
})
class SearchBarComponent implements OnInit {
results$: Observable
subject = new Subject()
constructor(private httpClient: HttpClient) {}
ngOnInit() {
this.results$ = this.subject.pipe(
debounceTime(1000),
map(searchText => this.httpClient.get("/api/search?q=" + searchText))
)
}
search(evt) {
const searchText = evt.target.value
// emits the `searchText` into the stream. This will cause the operators in its pipe function (defined in the ngOnInit method) to be run. `debounce` runs and then `map`. If the time interval of 1 sec in debounce hasn't elapsed, map will not be called, thereby saving the server from being called.
this.subject.next(searchText)
}
}
Same but simpler.
The lodash and underscore utility libraries export the debounce function that we can use to debounce methods/functions execution.
underscore
Let's see how we can use the debounce in the underscore library to debounce our search function:
// ...
import * as _ from 'underscore';
@Component({
...,
template: `
<input type="text" placeholder="Search" (keyup)="search(evt)"
<div class="search-result" *ngFor="let result of results$ | async">
//...
</div>
`
})
class SearchBarComponent {
results$: Observable
constructor(private httpClient: HttpClient) {
this.search = _.debounce(this.search, 1000)
}
search(evt) {
const searchText = evt.target.value
this.results$ = this.httpClient.get("/api/search?q=" + searchText)
}
}
We imported all the exported functions from underscore as _
. Then, in the constructor, we called the _.debounce
function passing in our search method and delay time of 1 sec, and re-assigning the returned debounced-version of this.search to the search method. With this, the search method has become debounced, it will not call the server until 1 sec has elapsed.
Let's repeat the same with lodash
lodash
// ...
import { debounce } from 'lodash';
@Component({
...,
template: `
<input type="text" placeholder="Search" (keyup)="search(evt)"
<div class="search-result" *ngFor="let result of results$ | async">
//...
</div>
`
})
class SearchBarComponent {
results$: Observable
constructor(private httpClient: HttpClient) {
this.search = debounce(this.search, 1000)
}
search(evt) {
const searchText = evt.target.value
this.results$ = this.httpClient.get("/api/search?q=" + searchText)
}
}
Same as what we did using underscore.
We can leverage decorators to debounce methods in our Angular components. Angular already made heavy use of decorators, so using decorators for debouncing methods would not be off.
import { debounce } from 'lodash';
/**
* Debounce a method
*/
function Debounce(ms) {
return function(target: any, key: any, descriptor: any) {
const oldFunc = descriptor.value
const newFunc = debounce(oldFunc, ms)
descriptor.value = function() {
return newFunc.apply(this, arguments)
}
}
}
import * as _ from 'underscore';
/**
* Debounce a method
*/
function Debounce(ms) {
return function(target: any, key: any, descriptor: any) {
const oldFunc = descriptor.value
const newFunc = _.debounce(oldFunc, ms)
descriptor.value = function() {
return newFunc.apply(this, arguments)
}
}
}
We can then decorate the search
method with the Debounce decorator:
@Component({
...,
template: `
<input type="text" placeholder="Search" (keyup)="search(evt)"
<div class="search-result" *ngFor="let result of results$ | async">
//...
</div>
`
})
class SearchBarComponent {
results$: Observable
constructor(private httpClient: HttpClient) {
}
@Debounce(1000)
search(evt) {
const searchText = evt.target.value
this.results$ = this.httpClient.get("/api/search?q=" + searchText)
}
}
Simple
We have seen ways by which we can debounce or delay HTTP requests in an Angular app. RxJS, lodash and underscore libraries provide a great and safe way we can use to prevent unwanted HTTP requests from our Angular app.
Delaying HTTP requests shuts out unnecessary server requests, preventing a function/method/action both client- and server-side from being called over and over rapidly.
If you have any questions regarding this or anything I should add, correct or remove, feel free to comment, email or DM me.
Thanks !!!