Let us start with depicting the usage of both ways of data retrieval with a high-level diagram:
Commonly, we work with observable data in a component class. One such classic example is CRUD operations on a REST API. For this, mostly we use Angular’s HttpClient class’s methods to perform various HTTP request operations, which returns HTTP responses as an observable.
This blog post discusses various ways of reading observable data in a component and their characteristics. Predominately there are two ways to read observable data,
- Using the async pipe
- By subscribing to the observable using the subscribe() method
We will discuss both these options and their behavior corresponding to a particular change detection strategy. To refresh, Angular has two Change Detection strategies
- default
- onPush
In the onPush strategy, Change Detection runs when a new reference is being passed to the component.
Fetching data from the API
Let us say we have an API that returns Product data as JSON, and we have a requirement to display the returned data in a component. For understanding, the Product data looks like below,
To work with the API data, first, we create an entity class as shown next, where each properties crosspond to the columns of data returned,
export class Product{ public ProductID : string; public Title : string; public Price : number ; public Color : string; public inStock: string; public Details: string; public Quantity: number; public Rating: number; public ExpiryDate: string; public ImageUrl: string; }
And after that, we create an Angular Service and inside service initially do the following tasks,
- Inject HttpClient service
- Configure HTTP header
- Configure HTTP request options
These tasks can be done using the code listed below,
export class ProductService { // API URL apiurl = "https://localhost:44316/api/products"; // Setting request headers to JSON headers = new HttpHeaders() .set('Content-Type', 'application/json') .set('Accept', 'application/json'); httpOptions = { headers: this.headers }; constructor(private http: HttpClient) { } // rest of the code ... }
So far, we have created an HTTP Request Header and injected HttpClient Service. Next, to fetch data from the API, we perform HTTP GET operation.
The HttpClient service has a various method to perform various HTTP operations, among them, get method performs HTTP GET operation. We can use get method as shown next,
getProducts(): Observable<Product[]> { return this.http.get<Product[]>(this.apiurl, this.httpOptions) .pipe( tap(data => { // debug here console.log(data); }), catchError(this.handleError) ); }
The getProducts() function either returns an observable of product array or returns throwError observable.
The HttpClient service’s get method returns HttpResponse as observable with the response body is set to the request type. This means it does content negotiation with the API, and if Content-Type is set to JSON, it returns a response as JSON.
We have also used RxJS operators,
- tap : to tap in the input stream and log the returned data
- catchError : to handle the error in the HTTP request and throw an error as observable
The handeError function returns a creational function throwError, which creates a stream of the error.
private handleError(error: any) { return throwError(error); }
So far, Angular services have a function in which we are making HTTP Request to the API and return HTTP response. Putting everything together service will look like next code listing,
import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Product } from './product.enitity'; import { Observable, throwError } from 'rxjs'; import { tap, catchError } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class ProductService { // API URL apiurl = "https://localhost:44316/api/products"; // Setting request headers to JSON headers = new HttpHeaders() .set('Content-Type', 'application/json') .set('Accept', 'application/json'); httpOptions = { headers: this.headers }; constructor(private http: HttpClient) { } getProducts(): Observable { return this.http.get(this.apiurl, this.httpOptions) .pipe( tap(data => { // debug error here console.log(data); }), catchError(this.handleError) ); } private handleError(error: any) { return throwError(error); } }
Using the subscribe() method
Everything good so far. Now in the component class, to use ProductService first, you inject it and create variables to stored returned data
export class ProductsComponent implements OnInit, OnDestroy { products: Product[] ; productsubscription: Subscription; constructor(private productservice: ProductService) { }
The products variable will store returned data from the service, and productsubscription variable will store the observable. Data can be read from the ProductService as shown in the next code listing.
getProducts() { this.productsubscription = this.productservice.getProducts().subscribe( (data) => {this.products = data;}, (error) => { console.log(error) }, () => { console.log(`products stream completed`); } ); }
We are simply subscribing to the getProducts() method of the service and reading the returned data in the products variable. We have also handled the error and complete functions by logging respective information.
Putting everything together the component class which subscribes to the HTTP response using a service looks like next code listing,
@Component({ selector: 'app-products', templateUrl: './products.component.html', styleUrls: ['./products.component.css'], changeDetection:ChangeDetectionStrategy.Default }) export class ProductsComponent implements OnInit, OnDestroy { products: Product[] ; productsubscription: Subscription; constructor(private productservice: ProductService) { } ngOnInit() { this.getProducts(); } getProducts() { this.productsubscription = this.productservice.getProducts().subscribe( (data) => {this.products = data;}, (error) => { console.log(error) }, () => { console.log(`products stream completed`); } ); } ngOnDestroy() { this.productsubscription.unsubscribe(); } }
A couple of important points worth discussing the ProductsComponent class are,
- The Change Detection Strategy of the component is set to default
- The component is explicitly unsubscribing to the observable in the ngOnDestroy() life cycle hook.
- If the component subscribes to many observables, then we have to manually unsubscribe them, and not doing so may cause memory leaks and may have a performance impact
Keeping the above point in mind, implementation in the ProductComponent using the subscribe method does its task perfectly to read observable data from the service. On the template, data is displayed as shown next:
<h2>Product Loading .....</h2> <div> <table> <thead> <tr> <th>Id</th> <th>Title</th> <th>Price</th> <th>Color</th> <th>Quanity</th> <th>Rating</th> <th>In Stock</th> </tr> </thead> <tbody> <tr> <td>{{p.ProductID}}</td> <td>{{p.Title}}</td> <td>{{p.Price}}</td> <td>{{p.Color}}</td> <td>{{p.Quantity}}</td> <td>{{p.Rating}}</td> <td>{{p.inStock}}</td> </tr> </tbody> </table> </div>
Everything is fine in the above implementation, and you should get data displayed in the table.
So far, so good. Now go ahead and set component’s change detection strategy to onPush.
Now you find that data is not displayed in the table, and instead of that you are getting Product Loading message.
Since getProducts() does not return new reference and change detector is set to onPush, Angular does not run the change detector. You can solve this by manually instructing Angular to run a change detector. To do that,
- Inject ChangeDetectorRef service in the component
- Use markForCheck in the subscription method to instruct Angular to check the component by running the change detector.
Now you will find data is rendered in the table again. Above we are manually marking the component for the change and also manually unsubscribe when components get destroyed.
Advantages of subscribe() approach are,
- Property can be used at the multiple places in the template
- Property can be used at the multiple places in the component class
- You can run custom business logic at the time of subscribing to the observable.
Some of the disadvantages are,
- For the onPush change detection strategy, you have to explicitly mark component to run the change detector.
- Explicitly unsubscribe the observables.
This approach may go out of hand when there are many observables used in the component. If we miss unsubscribing any observable, it may have potential memory leaks, etc. Nevertheless, putting everything together,
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; import { ProductService } from '../product.service'; import { Product } from '../product.enitity'; import { Observable, Subscription } from 'rxjs'; @Component({ selector: 'app-products', templateUrl: './products.component.html', styleUrls: ['./products.component.css'], changeDetection:ChangeDetectionStrategy.OnPush }) export class ProductsComponent implements OnInit, OnDestroy { products: Product[] ; productsubscription: Subscription; constructor(private productservice: ProductService, private cd: ChangeDetectorRef) { } getProducts() { this.productsubscription = this.productservice.getProducts().subscribe( (data) => { this.products = data; this.cd.markForCheck(); }, (error) => { console.log(error) }, () => { console.log(`products stream completed`); } ); } ngOnInit() { this.getProducts(); } ngOnDestroy() { this.productsubscription.unsubscribe(); } }
Async pipe
The second or some people consider it a better approach is to work with observable data is by using async pipe. To use async pipe,
- Declare a variable of observable type
- Call the service method which returns observable
First, declare a variable like below,
products$ : Observable;
and then make a call to the service,
getProducts() { this.products$ = this.productservice.getProducts(); }
And on the template, use an async pipe to display the observable data, as shown next
<h2>Product Loading .....</h2> <div> <table> <thead> <tr> <th>Id</th> <th>Title</th> <th>Price</th> <th>Color</th> <th>Quanity</th> <th>Rating</th> <th>In Stock</th> </tr> </thead> <tbody> <tr> <td>{{p.ProductID}}</td> <td>{{p.Title}}</td> <td>{{p.Price}}</td> <td>{{p.Color}}</td> <td>{{p.Quantity}}</td> <td>{{p.Rating}}</td> <td>{{p.inStock}}</td> </tr> </tbody> </table> </div>
We used async pipe in *ngFor directive to display the observable data. Main advantages of using the async pipe are
- In onPush change detection strategy, if observable data changes it automatically marks component for the check.
- On component destruction, it automatically unsubscribes the observable hence avoids chances of any potential memory leak
Using the async pipe keeps code cleaner, and also you don’t need to manually run change detector for onPush Change Detection strategy. To use async pipe component class will look like below,
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { ProductService } from '../product.service'; import { Product } from '../product.enitity'; import { Observable, Subscription } from 'rxjs'; @Component({ selector: 'app-products', templateUrl: './products.component.html', styleUrls: ['./products.component.css'], changeDetection:ChangeDetectionStrategy.OnPush }) export class ProductsComponent implements OnInit { products$ : Observable; constructor(private productservice: ProductService) { } getProducts() { this.products$ = this.productservice.getProducts(); } ngOnInit() { this.getProducts(); } }
Now, I hope you have a better understanding of when to use subscribe method approach and when to use async pipe. So, to summarize let us revisit the diagram we shared in the beginning of this post,
My suggestion is when in doubt use async pipe 😊. I hope you find this post useful. Thanks for reading.
I am an indepnedent Trainer and Consultant on Angular, JavaScript, Node, .NET Core, REST , GraphQL . To hire reach me at debugmode@outllook[dot]com or tweet me @debug_mode
Leave a Reply