Since the introduction of Angular2 there has been some confusion over when to use promises and when to use observables in our applications, and there's also been some confusion over how these two approaches compare.
If you're new to Promises, read an earlier post for an introduction.
Promises work with asynchronous operations and they either return us a single value (i.e. the promise resolves) or an error message (i.e. the promise rejects).
Another important thing to remember regarding promises is that a request initiated from a promise is not cancellable. (There are some Promise implementation libraries that allow us to cancel promises but, the JavaScript native Promise implementation does not allow for this).
And therelin lies one of the key differences between using a promise or an observable. Since we can't cancel a promise, an HTTP request that does a search for example on keyup would be executed as many times as we press the key.
Here's a very simple example demonstrating that:
<!-- app.component.html -->
<div>
<h2>Star Wars Character Search</h2>
<input #term type="text" (keyup)="search(term.value)" />
<hr />
<div *ngIf="results.length > 0">
<li *ngFor="let result of results">
is a character with a height of .
</li>
</div>
</div>
// app.component.ts
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
private results = [];
constructor(private http: HttpClient) {}
private search(term) {
console.log(term);
this.http
.get(`https://swapi.co/api/people/?search=${term}`)
.toPromise()
.then((data: any) => {
/* tslint:disable:no-console */
console.time('request-length');
console.log(data);
console.timeEnd('request-length');
this.results = data.results;
});
}
}
The console
statements are left in there deliberately. This is how the browser console would look like:
[Log] l
[Log] lu
[Log] luk
[Log] luke
[Log] {count: 2, next: null, previous: null, results: Array}
[Debug] request-length: 0.590ms
[Log] {count: 37, next: "https://swapi.co/api/people/?page=2&search=l", previous: null, results: Array}
[Debug] request-length: 0.281ms
[Log] {count: 1, next: null, previous: null, results: Array}
[Debug] request-length: 0.377ms
[Log] {count: 1, next: null, previous: null, results: Array}
[Debug] request-length: 0.263ms
Notice, for each keypress, there's a request going out - so for the search term 'luke' we have excatly 4 requests. This is really not ideal.
Promises do have their use-cases, we can utilise them when making requests that for sure won't be cancelled. Let's also not forget that Promises can make use of async/await
functionality which can further help us to write asynchronous code.
Let's take a look at the observable approach as well.
If you're new to Observables, read this introductory article.
An observable is essentially a stream (a stream of events, or data) and compared to a Promise, an Observable can be cancelled. It out of the box supports operators such as map()
and filter()
.
Angular uses Rx.js Observables, and it uses out of the box when dealing with HTTP requests instead of Promises. That's why in the previous example we had to specify
toPromise()
to convert the Observable to a Promise
Let's rework the previous example to see how we can make our request to the API cancellable and therefore making sure that there's only one request going out to the service instead of 4 when the user is still typing.
The first change that we need to make to use ReactiveForms
. This will allow us to subscribe for changes happening in the input box itself:
<!-- app.component.html -->
<div>
<h2>Star Wars Character Search</h2>
<input [formControl]="term" />
<hr />
<div *ngIf="results.length > 0">
<li *ngFor="let result of results">
is a character with a height of .
</li>
</div>
</div>
// app.component.ts
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormControl } from '@angular/forms';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
private results = [];
private term = new FormControl();
constructor(private http: HttpClient) {}
ngOnInit() {
this.term.valueChanges
.debounceTime(400)
.distinctUntilChanged()
.subscribe((searchTerm) => {
this.http
.get(`https://swapi.co/api/people/?search=${searchTerm}`)
.subscribe((data: any) => {
/* tslint:disable:no-console */
console.time('request-length');
console.log(data);
console.timeEnd('request-length');
this.results = data.results;
});
});
}
}
Notice the usage of debounceTime()
and distinctUntilChanged()
methods. The first method allows us to discard emitted values between outputs that take less than 400 milliseconds., while the second method - as its name suggests - will show us a value, until that value has changed.
We then subscribe to these changes, and then we make the call out to the API and we also subscribe to the results coming back from the API. Check the logs now, and we can see that there's only one, single outgoing request:
[Log] {count: 1, next: null, previous: null, results: Array}
[Debug] request-length: 0.630ms