Nitay Neeman

Angular – Using Single Subscription for Multiple Async Pipes

The Issue

Let’s imagine we’ve an observable which fetches a list of users:

@Component({
  // Metadata
})
export class AppComponent implements OnInit {
  public users$: Rx.Observable<any>;

  ngOnInit() {
    this.users$ = Rx.Observable.of([
      { id: 1111, name: 'User 1', email: 'u1@dummy.com' },
      { id: 2222, name: 'User 2', email: 'u2@dummy.com' },
      { id: 3333, name: 'User 3', email: 'u3@dummy.com' }
    ]);
  }
}

All right. Suppose we got a mission to display each user field by a separated list. But there is a constraint, each list will be displayed differently. Thus, the appropriate template could be:

IDs:
<ul>
  <li *ngFor="let user of users$ | async">{{ user.id }}</li>
</ul>

Names:
<ol type="1">
  <li *ngFor="let user of users$ | async">{{ user.name }}</li>
</ol>

Emails:
<ol type="A">
  <li *ngFor="let user of users$ | async">{{ user.email }}</li>
</ol>

Let’s check how many subscriptions would be created in that way by adding a log when a subscription is created:

this.users$ = Rx.Observable.of([...])
                .do(() => console.info('Subscription is created')):

Here are the results of the Console:

Three different subscriptions were created
Three different subscriptions were created

Well, we notice that each usage of async pipe creates another subscription. Although these are three different *ngFor, we’re talking about the same observable – so we’d expect that async pipe would use a single instance of a shared subscription.

In case there is an operator on the observable, for example, a map operator which does a heavy business logic like calculating – that operator will be executed for each subscription individually.

Let’s see how to create a single subscription for users$ but still be able to use its result for each list.

The Solution

As part of Angular 4.0.0 – we introduced with the as keyword. That keyword enables assigning a local variable in the component’s template. In other words, it means we’re able to use a variable (as much as we want) which points to the evaluated result of the async pipe.

Note: In general, assigning the result using the as keyword isn’t necessarily for the async pipe but for all pipes.

To bind a local variable with the template, we need to wrap the template with the ng-container directive.

In case that you’re not familiar with the ng-container – it’s just a wrapper that enables to group multiple DOM elements (without adding an additional element to the DOM) and apply a structural directive on these elements (such as *ngFor and *ngIf).

It’s time to use the as keyword:

<ng-container *ngIf="users$ | async as users">
  IDs:
  <ul>
    <li *ngFor="let user of users">{{ user.id }}</li>
  </ul>

  Names:
  <ol type="1">
    <li *ngFor="let user of users">{{ user.name }}</li>
  </ol>

  Emails:
  <ol type="A">
    <li *ngFor="let user of users">{{ user.email }}</li>
  </ol>
</ng-container>

Basically, what we do is to assign the result from the subscription as a local variable which is named users. Notice we need a structural directive in order to interpret the as keyword and for binding the users variable with the result from the async pipe. That’s the reason we use *ngIf.

If we’d like to use the users array for other purposes (for instance, printing its length) we could use the users variable as well – as long as it’s evaluated inside that ng-container scope.

Here’s attached the final result:

Indeed, we’re done.