DEV Community

Cover image for How to simplify multiple async pipes
Dany Paredes
Dany Paredes

Posted on • Updated on • Originally published at danywalls.com

How to simplify multiple async pipes

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

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

For example, our app shows the user's name and the player stats, each of them came from another api.

  playerNumber = 237;
 player$ = this.nbaService.getPlayer(this.playerNumber);
  stats$ = this.nbaService.getStats(this.playerNumber);
Enter fullscreen mode Exit fullscreen mode

The template looks like:

  <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>
Enter fullscreen mode Exit fullscreen mode

How can combine our observable into a single observable?

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

CombineLatest only emit 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 map to return an object with clean name about each value to use in the template.

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

Update the template to use the pipe only for the playerData , 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>
Enter fullscreen mode Exit fullscreen mode

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 for the 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 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;
}
Enter fullscreen mode Exit fullscreen mode

The app.component process the data in the observable 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],
      };
    })
  );
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

Feel free to play with the demo

Photo by MichaΕ‚ Parzuchowski on Unsplash

Discussion (4)

Collapse
layzee profile image
Lars Gyrup Brink Nielsen

Hey Dany! πŸ‘‹

What if we used a presentational component that didn't deal with observables at all? A container component could deal with those pesky observables πŸ˜ƒ What if we used RxAngular Template to avoid the AsyncPipe null defect and NgIf hack? πŸ€”

@Component({
  // (...)
  selector: 'nba-player',
  template: `
    <h1>Nba</h1>

    <h2>{{ info?.first_name }} {{ info?.last_name }}</h2>

    <h3>Stats</h3>

    <ul>
      <li *ngFor="let stat of stats">
        Points: {{ stat.pts }} Rebounds: {{ stat.reb }} Steals: {{ stat.stl }}
      </li>
    </ul>
  `,
})
export class PlayerComponent {
  @Input()
  info: PlayerInfo | null = null;
  @Input()
  stats: PlayerStats = [];
}
Enter fullscreen mode Exit fullscreen mode
@Component({
  // (...)
  template: `
    <!-- Using RxAngular PushPipe to avoid null -->
    <nba-player
      [info]="info$ | push"
      [stats]="stats$ | push"
    ></nba-player>

    <!-- Or: Using RxAngular LetDirective to conditionally render
      when observables have resolved and avoid null -->
    <ng-container *rxLet="info$ as info">
      <nba-player *rxLet="stast$ as stats"
        [info]="info"
        [stats]="stats"
      ></nba-player>
    </ng-container>
  `,
})
export class PlayerContainerComponent {
  #playerNumber = 237;

  info$ = this.nbaService.getPlayer(this.#playerNumber);
  stats$ = this.nbaService.getStats(this.#playerNumber);

  constructor(private nbaService: NbaService) {}
}
Enter fullscreen mode Exit fullscreen mode
Collapse
danywalls profile image
Dany Paredes Author

Amazing refactor, wide better I need read again you article about presentation container and embrace it :P
dev.to/this-is-angular/container-c...
and dev.to/this-is-angular/presentatio...

Collapse
layzee profile image
Lars Gyrup Brink Nielsen

There's no one best way πŸ™‚ But it's good to have alternatives.

Thread Thread
danywalls profile image
Dany Paredes Author • Edited on

Updated with you feedback :D