Combine Async Pipes in Angular: How to Avoid Multiple Pipes

Avoiding Multiple Async Pipes in Angular

In Angular it is very common to subscribe to multiple observables to show data in our template, and use these observables in our template, we use multiple async pipes.

async pipe, make easy to subscribe and unsubscribe from the observable in our templates.

For example, our app shows the username and the player stats, each coming from another API.

  playerNumber = 237;
 player$ = this.nbaService.getPlayer(this.playerNumber);
  stats$ = this.nbaService.getStats(this.playerNumber);

The template looks like this:

  <div *ngIf="player$ | async as player" class="player">
    <h2>{{ player.first_name }} {{ player.last_name }}</h2>
    <h3>Stats</h3>
    <ul *ngIf="stats$ | async as stats">
      <li *ngFor="let stat of stats.data">
        Points: {{ stat.pts }} Rebounds: {{ stat.reb }} Steals: {{ stat.stl }}
      </li>
    </ul>
  </div>

How can we combine our observable into a single observable?

Rxjs provide combineLatest, which returns an array of each observable.

CombineLatest only emits until all observable emit one value; we want to show when the player$ and stats$ emit a value.

Create a new observable like player$, and it will contain properties for each observable,

Pipe the values from combineLatest, pipe them with a map to return an object with a clean name about each value to use in the template.

  playerData$ = combineLatest([this.player$, this.stats$]).pipe(
    map(([info, stats]) => ({ info, stats }))
  );

Update the template to use the pipe only for the playerData , and remove the ngIf and extra async pipe.

<div class="container">
  <h1>Nba</h1>
  <div *ngIf="playerData$ | async as playerData">
    <h2>{{ playerData.info.first_name }} {{ playerData.info.last_name }}</h2>
    <h3>Stats</h3>
    <ul>
      <li *ngFor="let stat of playerData.stats.data">
        Points: {{ stat.pts }} Rebounds: {{ stat.reb }} Steals: {{ stat.stl }}
      </li>
    </ul>
  </div>
</div>

We have a single observable to manage both subscriptions. Use combineLatest to merge and combine the data and use the template.

Part II, Improving the code.

Thanks to @layzee feedback, We can improve the code using:

  • Use a presentational component user-profile

  • Convert the app component into a container component to deal with the observable process and process data.

Read more about Container Components Read more about presentational components

Create a presentational component player-profile

We create the component app-player-info only to show the data using input properties.

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-player-info',
  templateUrl: './player-info.component.html',
  styleUrls: ['./player-info.component.css'],
})
export class PlayerInfoComponent {
  @Input() name: string;
  @Input() stats: any;
}

The app.component processes the observable data using the map rxjs operator to simplify stats.data array to a single object using destructuring.

Read more about Rxjs map operator. Read more about destructuring object.

 stats$ = this.nbaService.getStats(this.playerNumber).pipe(
    map((value) => {
      return {
        ...value.data[0],
      };
    })
  );

Edit the template, use the player-profile component and bind the properties.

<div class="container">
  <h1>Nba</h1>
  <div *ngIf="playerData$ | async as player">
    <app-player-info
      [name]="player.info.first_name"
      [stats]="player.stats"
    ></app-player-info>
  </div>
</div>

Our code has a separation between processing the data and showing the information.

Feel free to play with the demo

Photo by Michał Parzuchowski on Unsplash