Angular Observables and Promises – How to Use Them
Angular makes it easy to build web applications and RxJS is about bringing the principles of functional programming to JavaScript.
Join the DZone community and get the full member experience.
Join For FreeAngular, as we all know, is a JavaScript framework that makes it easy to build web applications. RxJS is really about bringing the principles of functional programming to JavaScript.
Functional Programming in JavaScript?
It all started with the rise of asynchronicity or asynchronous JavaScript, where we have data that is flowing through our application coming from different sources, for example, APIs, Web sockets and User-triggered events and timers. We have a situation where we can’t control when any of this data is sent through our application. Neither can we decide the order in which it happens, so having a more functional paradigm would help resolve that. You want your application to be more predictable, but the scaling up of applications that run client side means less predictability.
So then comes the rise of Promises, which are supposed to solve problems we’ve all had with XHRs, which came to save us from the callback hell. You will discover that the more you use Promises with their then and catch functions, the more they look like callbacks again.
Few Problems With Promises
- A Promise is defined where the data is created, not where it is being consumed.
- As your application gets bigger, Promises become hard to manage.
- What if I want to retry a failed call? Now we are back to callback hell again.
Then Observables Arrived
RxJS is all about unifying the ideas of Promises, callbacks and data flow, and making them easier to work with.
An Observable is an array or a sequence of events over time. It has at least two participants. The creator (the data source) and the subscriber (subscription – where data is being consumed).
For example, let data = http.get(‘/api.json’). In Angular, data is going to be an Observable of responses, because the HTTP.get method returns a Promise.
Search YouTube API Using Angular With an Observable
To demonstrate the power of an Observable with Angular, we will create a sample project, querying the YouTube API.
Create a New Angular App
Open Angular IDE, click File in the top menu, then select New, then click Angular Project.
This will take us to a page where we can specify the project details, such as name, version of the Angular-CLI and Node to use. Now enter youtube-searcher
as the project name, 1.6.8 as the Angular CLI version, 9.5.0 as the Node.js version, and 5.6.0 as the npm version. Click Next, then click Finish.
Get an API Key From Google Console
To interact with the YouTube API, we need to get an API Key from the Google Console, so navigate here and follow the steps to obtain your API Key. You will have to create an app to obtain API credentials for it. This will enable our app to submit API requests to the YouTube API.
Click on Create credentials
to create credentials for your app.
After obtaining the API key, navigate back to your project in the Angular IDE and create a TypeScript src
file in the app folder.
You can name the file api-key.ts
, and this will be its contents:
const API_KEY = 'your-api-key-here'; export default API_KEY;
After this, you will import the API key into the app.component.ts
file which is where we will write the code to query the YouTube API. Do it like this:
import API_KEY from './api-key';
Query the YouTube API
The YouTube API doesn’t return a direct list of results, but rather a group of metadata where it has one property called Items that contains the list of results.
Next, let’s build our form in the src/app/app.component.ts file. Add the following code:
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormBuilder, Validators, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs/Observable';
import API_KEY from './api-key';
const API_URL = 'https://www.googleapis.com/youtube/v3/search';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'Angular RxJs YouTube Searcher';
searchForm: FormGroup;
results: any;
constructor(private formBuilder: FormBuilder, private http: HttpClient) {
this.searchForm = this.formBuilder.group({
search: ['', Validators.required]
});
this.searchForm.controls.search.valueChanges.subscribe(result => console.log(result));
}
}
The Search Input Form is using the Angular Reactive Forms API. Learn more about reactive forms here.
Because the valueChanges
method returns an Observable, here in our example it returns an Observable of characters typed in the input element. The subscribe function call subscribes to each value, saves them in the result variable, and displays that in the browser console with console.log.
We are also using the arrow function expression (=>
) on the subscribe call, which has a shorter syntax than the function expression. It’s very efficient when you want to write short functions like the type you would use in a subscribe method. Read more about arrow function here.
Using the Angular Reactive Form API
To start using the Reactive Forms API and HTTP Client, we need to import the modules into the app.module.ts
file, so update the module with the following imports and add the modules to the imports array:
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
.....
imports: [
BrowserModule,
ReactiveFormsModule,
HttpClientModule
],
......
And in the template file, src/app/app.component.html, add the following code:
<h3>{{ title }}</h3>
<form [formGroup]="searchForm">
<label for="search">Search YouTube</label>
<br />
<input formControlName="search" id="search" />
</form>
Also note that the searchForm
and search both correspond to the names given in the template because that’s how Angular knows how to connect the template HTML code to the TypeScript component code.
Running the Angular App
Let’s run the Angular app through the server view in Angular IDE. In order to open it, select Window, then Show view, then Servers. Right click on youtube-searcher
and click Start Server. Angular IDE serves the application on localhost port 4200 by default, so open up in your browser to see the running app.
We won’t be doing anything with our search, for now, we just need to verify that the form is working. To open up the Console, right-click anywhere on the page of your browser, click Inspect, and select the Console tab.
As you can see, everything that is typed into the Input form gets logged out – that’s the first example showing how Angular leverages the power of the Observable method.
Next, we need to pass that value and use it to query the YouTube API for results. Let’s do that next. Update the search section in the file to look like this (we will console.log the value that we get from the YouTube API).
this.searchForm.controls.search.valueChanges.subscribe(searchTerm => {
this.http.get<any>(
`${API_URL}?q=${searchTerm}&key=${API_KEY}&part=snippet`)
.subscribe(result => {
console.log(result);
});
});
The result should look like this when you now try to search. As you can see, we are getting back the responses from the API, and if you explore well, you will see the Items object in the response.
Improve and Refactor the Code
However, we now have some issues. First, you will notice that currently, we query the API every time we type in a word into the input form. This is not what we would want, else we would quickly exceed our usage and query limit. Therefore, we will use some operators from the RxJS library to clean up the code and achieve the desired result.
First, let’s move the whole search function into a separate method. Let’s call it search
. Then we will call it inside ngOnInit
instead of in the constructor, as we are currently doing. In a larger Angular app, it is a bad practice to call functions, especially Angular functions, inside the constructor. You don’t want to call any of your Angular functions inside the constructor because Angular does not have any control over when the constructor gets called or initiated on page load. You can read more here on the difference between ngOnInit and the constructor.
After refactoring the code, the result looks like this. Notice the importation of OnInit
from @angular/core
on line 1 and its implementation on line 20. After this update, your app’s behavior should remain the same. Go ahead and give it a try.
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormBuilder, Validators, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs/Observable';
import API_KEY from './api-key';
const API_URL = 'https://www.googleapis.com/youtube/v3/search';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title = 'Angular RxJs YouTube Searcher';
searchForm: FormGroup;
results: any;
constructor(private formBuilder: FormBuilder, private http: HttpClient) {
this.searchForm = this.formBuilder.group({
search: ['', Validators.required]
});
}
ngOnInit() {
this.search();
}
search() {
this.searchForm.controls.search.valueChanges.subscribe(searchTerm => {
this.http.get<any>(
`${API_URL}?q=${searchTerm}&key=${API_KEY}&part=snippet`)
.subscribe(result => {
console.log(result);
});
});
}
}
So back to getting the desired result, our first issue is with the nested subscription, which is common when using Promises. This sort of nesting is called the pyramid of doom, which is one of the major reasons we don’t want to use *a* Promise in the first place. *We* want to avoid that at all *costs*, and to do that, Observable provides an operator called Pipeable Operator, which allows you to *do this* nesting of operations in a more efficient way.
Thus, we will refactor our code to leverage the pipe. The first operator we will use is the switchMap
operator.
Understanding Operators in RxJS
switchMap
will cancel out the prior event and just use the value in the latest event. With switchMap
, every time a new Observable is returned, it unsubscribes from the previous one and subscribes to the latest one. Basically, it's switching from one Observable to the latest Observable, while mergeMap
will let all the Observables go (it won’t cancel the old ones – this is where you could get the race conditions, which results in a weird behavior where the result of the first event could be returned instead of the desired one). Read more about race conditions and about the different types of Observable maps.
Now, with the application of switchMap
, should you test this out, you will notice that the request is not sent to the API upon every keystroke anymore.
import { switchMap } from 'rxjs/operators';
search() {
this.searchForm.controls.search.valueChanges.pipe(
switchMap(searchTerm => this.http.get<any>(
`${API_URL}?q=${searchTerm}&key=${API_KEY}&part=snippet`))
).subscribe(result => console.log(result));
}
But something still seems off. If you are on a fast network, you will notice that at least two objects are still being returned, which means the subscription didn’t wait for you to finish typing your query before the request was sent. We can delay the sending of request a little bit by adding another operator called debounceTime, a really helpful operator that will debounce the events. It will discard emitted values that take less than the specified time between outputs.
import { switchMap, debounceTime } from 'rxjs/operators';
search() {
this.searchForm.controls.search.valueChanges.pipe(
debounceTime(500),
switchMap(searchTerm => this.http.get<any>(
`${API_URL}?q=${searchTerm}&key=${API_KEY}&part=snippet`))
).subscribe(result => console.log(result));
}
So switchMap
canceled the prior request but only at the browser level. Once those network requests go out, they are still hitting the server, which means an unwanted request might still be sent. Now debounce will make the request wait 500 milliseconds until the events are fired (until requests are sent to the server) – events will not be sent to the server until you have finished typing in the request. This means only one API call/request will go to the server.
We can also add other operators like filter and distinctUntilChanged. Filter only emits those items from an Observable that pass a predicate test, while distinctUntilChanged
only emits when the current value is different from the last.
We also have to apply a map operator to map through the values that are returned from the API, because the request returns a big object with different metadata, and the only object we need from this is the Items object.
The final code looks like this:
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormBuilder, Validators, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs/Observable';
import { map, switchMap, debounceTime, distinctUntilChanged, filter } from 'rxjs/operators';
import API_KEY from './api-key';
const API_URL = 'https://www.googleapis.com/youtube/v3/search';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title = 'Angular RxJs YouTube Searcher';
searchForm: FormGroup;
results: Observable<any>;
constructor(private formBuilder: FormBuilder, private http: HttpClient) {
this.searchForm = this.formBuilder.group({
search: ['', Validators.required]
});
}
ngOnInit() {
this.search();
}
search() {
this.results = this.searchForm.controls.search.valueChanges.pipe(
debounceTime(500),
filter(value => value.length > 3),
distinctUntilChanged(),
switchMap(searchTerm => this.http.get<any>(`${API_URL}?q=${searchTerm}&key=${API_KEY}&part=snippet`)),
map(response => response.items)
);
}
}
Angular’s HTTP method returns an Observable instead of returning a Promise. This Observable then needs to be subscribed to for it to be consumed. Using the pipeable operator on the subscription, we are able to transform the returned subscription, and now we need to use an async pipe to consume the subscription on the template side.
To make Angular and RxJS better together, Angular has created an async pipe, used in the template.
While it’s possible to do a standard subscription on the component side, it can also be done in the declarative template style, which is generally recommended by Angular. Async will automatically subscribe to the Observable for you, and it will automatically unsubscribe for you as well when you navigate out of the page or component.
And in the HTML template we have this:
<h3>{{ title }}</h3>
<form [formGroup]="searchForm">
<label for="search">Search YouTube</label>
<br />
<input formControlName="search" id="search" />
<div *ngFor="let result of results | async">
<a [href]="'https://www.youtube.com/watch?v=' + result.id.videoId" target="_blank">
{{result.snippet.title}}
</a>
<p>{{result.snippet.description}}</p>
<img [src]="result.snippet.thumbnails.default.url" style="width: 100%; max-width: 250px;" />
</div>
</form>
What the async pipe does depends on whether you give it a Promise or an Observable. It creates and unwraps the subscription or Promise, and displays the data when the component is loaded, when the template is running, and then automatically unloads and unsubscribes when the component is unloaded, for example when you navigate to another page with a new component. This manages the whole life cycle of subscription to Observables, so you don’t have to be managing any of that yourself.
Your result should now look like this:
Points to take note of:
- With a Promise you can only handle one event.
- With an Observable you can handle multiple events.
.subscribe()
is similar to.then()
.- An Observable can do everything that a Promise can do, plus more.
- Use Angular’s HttpClient to handle API calls.
- The Angular framework uses a lot of RxJS.
- Many Angular APIs use Observables – we can take advantage of this to build some really cool stuff.
Conclusion
RxJS is a really important part of your Angular toolbox – even though it doesn’t solve every problem, it will definitely make you a better Angular developer. Asynchronous experiences are the norm for today’s websites. With the combined power of Angular and RxJS, you’re well on your way to delivering this experience to your users too.
Published at DZone with permission of George Anderson, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments