Skip to content

Instantly share code, notes, and snippets.

@philipszdavido
Created January 27, 2020 10:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save philipszdavido/a14b377d7764641197797b4ff2f56e12 to your computer and use it in GitHub Desktop.
Save philipszdavido/a14b377d7764641197797b4ff2f56e12 to your computer and use it in GitHub Desktop.

Ways to debounce Http requests in Angular

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

Using RxJS

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.

Using lodash/underscore debounce method

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.

Using decorators

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

Conclusion

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 !!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment