Using the async pipe within Angular templates to perform control flow

If you’re working on a small to mid-sized Angular app, chances are you’re probably not using a state management library like NgRx to handle your app’s state. And that’s probably alright if you’re only working with maybe a dozen or so components.

If that’s the case, then you’re probably relying on services to manage state as it’s the out-of-the-box method presented to us by the Angular team.

The gist of services is to abstract away any business logic from our components, and subscribing to any state that we need to use within the component.

One common task that we routinely find ourselves doing when relying solely on services for state management is creating semaphore variables to determine whether or not a certain piece of state is loading and as a result displaying some kind of message or icon to the user letting them know that it may be a moment before the data is ready. Error handling also mimics this same pattern where we’re using Subjects or BehaviorSubjects and subscribing to them over and over again. Based on the truthiness of a variable like “hasErrors”, we can display a message letting the user know something unexpected happened and it requires their attention.

The async pipe provided to us by Angular out of the box is a great way to get around having to set up numerous subscriptions within our component TS files, and comes in especially handy when you’re working on a small to mid-sized application that doesn’t have extremely complex state and control flow.

Let’s take a look at the traditional way of setting up a simple service and a method to fetch some data from an external API, and then subscribe to that data from within a component:

import { Injectable } from '@angular/core';
import { BehaviorSubject, of } from 'rxjs';
@Injectable({
  providedIn: 'root',
})
export class CounterService {
  count$: BehaviorSubject<number> = new BehaviorSubject<number>(0);
  isLoading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
  constructor() {}
  increase() {
    this.isLoading$.next(true);
    setTimeout(() => {
      this.count$.next(this.count.value + 1);
      this.isLoading$.next(false);
    }, 3500);
  }
}

The above service is somewhat contrived, but it’s a good example to show us how subscribing to the isLoading variable can be abstracted away using the async pipe within our component template files. Let’s take a look at how how we’d implement showing the user some kind of loading prompt within the use of the async pipe:

import { CounterService } from '../services/counter.service';
import { Component, OnInit } from '@angular/core';
@Component({
  selector: 'app-my-counter',
  templateUrl: './my-counter.component.html',
  styleUrls: ['./my-counter.component.scss'],
})
export class MyCounterComponent implements OnInit {
  count: number;
  isLoading: boolean;
  constructor(public countService: CountService) {}
  ngOnInit(): void {
      this.countService.count$.subscribe((count) => {
        this.count = count;
      })
      this.countService.isLoading$.subscribe((isLoading) => {
        this.isLoading = isLoading;
      })
    this.countService.increase();
  }
}

Lines 21 – 23 are where things start to get really repetitive and you’ll find yourself setting up at least 3 subscriptions per component, assuming that each component always has some kind of meaningful external state (In our example the count variable), and semaphore variables for both loading and errors.

Also note: Line 14 we’re also marking the counterService public instead of private like we typically do when we handle dependency injection via the constructor’s parameters. If we did not do this, the counterService would not be available within the associated template file that we’re going to work with next.

Let’s abstract that away from our component TS files and implement it within our template files instead using the async pipe:

<div *ngIf="counterService.isLoading$ | async; else showCount">
    Loading... Please wait!
</div>
<ng-template #showCount>
    {{ counterService.count$ | async }}
</ng-template>

What a difference! We no longer have to set up subscriptions within our component TS files but can instead lean on the async pipe to manage the subscriptions and assignments for us!

The async pipe is great because it subscribes to the observable stream and takes whatever values comes across the stream and interpolates it within the template without us having to specifically initialize a local template variable and handle the assignment based on the subscription.

Just to be clear, this will work when working with not-so-complex states generally found in smaller to mid-sized apps. This pattern starts to break down once you start introducing things like role based authentication, really in depth control flow, or a more complex object with nested objects and other data structures and collections.