Note: This post is part of an eleven-part series on Angular development. You can find the series overview here.

Overview

One of the most common patterns seen in Angular components is to subscribe to an Observable and display the resulting data in a component. As discussed in the Observables – Keeping Things Clean and Clear article, this can add up to some extra boilerplate code in managing the subscriptions and the eventual unsubscribe that is needed. To help cleanly manage these subscriptions, Angular provides the async pipe that can take care of all the overhead for us. Angular also provides some extended functionality of the ngIf directive in conjunction with the async pipe to quickly and easily create effective display components.

To follow along with the articles there a two repositories created: a starting point and final solution

Without the Async Pipe

To see what the async pipe provides, we will start with an example component that does not use the pipe.  This component calls a service to subscribe to an Observable which it displays directly, an subscriptions that needs modification before display, and a Promise.  First a look at the service:

 
@Injectable({
    providedIn: 'root'
})
export class SampleService {
    public sampleObservable(): Observable<number> {
        return interval(1000);
    }

    public samplePromise(): Promise<string> {
        return new Promise(resolve => {
            setTimeout(() => resolve('Hello from promise!'), 1500);
        });
    }
}

This service creates an Observable using the interval method.  This fires a counter value every time the interval provided elapses. So by providing 1000 (the parameter is milliseconds), we will get a new counter value every second.  The Promise that is generated is resolved with static text message after a 1500 millisecond timeout.

Now to look at our starting point component:

@Component({
    selector: 'app-manual-subscribe',
    template: `
        <h3>Manual Subscribe Data Load</h3>
        <div>Seconds since subscribe: {{counter}}</div>
        <div>Time at update: {{updateTime | date:'medium'}}</div>
        <div>Promise result: {{message}}</div>`
})
export class ManualSubscribeComponent implements OnInit, OnDestroy {
    public counter: number;
    public message: string;
    public updateTime: Date;
    private unsubscribe = new Subject<void>();

    constructor(private sampleService: SampleService) { }

    ngOnInit(): void {
        this.sampleService.sampleObservable()
            .pipe(takeUntil(this.unsubscribe))
            .subscribe(data => this.counter = data);

        this.sampleService.sampleObservable()
            .pipe(takeUntil(this.unsubscribe))
            .subscribe(() => this.updateTime = new Date());

        this.sampleService.samplePromise()
            .then(data => this.message = data);
    }

    ngOnDestroy(): void {
        this.unsubscribe.next();
        this.unsubscribe.complete();
    }
}
As you can see this is a fairly large component for the limited functionality we are providing due to the necessary boilerplate.  We have two subscriptions in the ngOnInit function – one to bind the new counter value and one to calculate a new date every second to display.  These subscriptions are managed as described in Observables – Keeping Things Clean and Clear by using the takeUntil method that triggers an unsubscribe in ngOnDestroy.  We also bind the result of the promise to a message property to display. Below we can see the results of this component before and after the async methods resolve.

Using the Async Pipe

With this starting point in place, we can bring in the async pipe to help clean everything up for us.  The async pipe is used in a template on any async method or property and Angular will handle the subscription, data binding, and unsubscribe for that reference.  So the first step is to provide a handle to the async objects instead of a static data value.  Below are the changes to the body of the component:

export class AsyncLoadComponent implements OnInit {
    public counter$: Observable<number>;
    public updateTime$: Observable<Date>;
    public message$: Promise<string>;

    constructor(private sampleService: SampleService) { }

    ngOnInit(): void {
        this.counter$ = this.sampleService.sampleObservable();
        this.updateTime$ = this.sampleService.sampleObservable().pipe(map(() => new Date()));
        this.message$ = this.sampleService.samplePromise();
    }
}
There a few specifics to note about the changes:

  • ngOnDestroy and the associated unsubscribe subject are gone – Angular handles the unsubscribe for us so we don’t need to worry about it
  • We declare Observable/Promise properties as a handle to provide the template to the async result.  You may often see async properties with the trailing $ on the variable name as a convention.  This is not required but can be a helpful convention to communicate synchronous vs. asynchronous values
  • We need to add a map function to the updateTime$ Observable to transform the data instead of transforming in the subscribe.  Here we don’t need the value from the Observable but it is available just as in the subscribe method.  We can pipe any function we want into the Observable stream to modify it before binding including map, filter, debounceTime, etc.

With these changes, we can implement the async pipe in the template:

<h3>Async Data Load</h3>
<div>Time since subscribe: {{counter$ | async}}</div>
<div>Time at update: {{updateTime$ | async | date:'medium'}}</div>
<div>Promise result: {{message$ | async}}</div>
All that is necessary is to add the async pipe in the data binding and Angular handles the rest.  You can also see in the updateDate$ binding that we can chain pipes to still provide date formatting – just be aware that order matters as data is transformed through the pipes.  The async pipe is also not limited to template binding, but can be used in binding data to child component inputs, directives, or anywhere else that you would normally bind data.  We can test these changes and see that we get the exact same results as our starting point:

Async and NgIf

Often it is desirable to show some loading indicator while we wait for an async value to resolve.  Let’s start with a trimmed down version of the component we created by looking at only an Observable and Promise.  We only want to display the associated text once the first data loads so we need to add some ngIf statements on the wrapper divs to hide them until we have some data:

@Component({
    selector: 'app-async-if',
    template: `
        <h3>Async Data Load With If</h3>
        <div *ngIf="counter$ | async">Time since subscribe: {{counter$ | async}}</div>
        <div *ngIf="message$ | async">Promise result: {{message$ | async}}</div>`
})
export class AsyncIfComponent implements OnInit {
    public counter$: Observable<number>;
    public message$: Promise<string>;

    constructor(private sampleService: SampleService) { }

    ngOnInit(): void {
        this.counter$ = this.sampleService.sampleObservable();
        this.message$ = this.sampleService.samplePromise();
    }
}

If we test, we can see that this works as expected:

However, this approach now means we are managing 4 separate async operations instead of referencing the result twice – once in the ngIf and once in the template data binding.  Here is where Angular provides extra functionality for ngIf to store the results of the conditional statement in a template variable for future use:

<div *ngIf="counter$ | async as result">Time since subscribe: {{result}}</div>
<div *ngIf="message$ | async as result">Promise result: {{result}}</div>
Test Scope: {{result}}
All that is needed is to provide “as <variable>” in the conditional statement to create the binding.  Let’s test this and discuss a few important points to consider:

 

  •  Note that both bindings use the same value which is also used later as a test.  The data binding is scoped to within the parent element governed by the ngIf statement.  Best practice would include better results names for the data to prevent confusion, but this is to demonstrate that there are not collisions between these template variables that are created.  Data also does not extend outside of the parent div, so if it is needed in multiple places you will need to place the binding at a higher level or create multiple subscriptions.
  • This pattern is also helpful to avoid the need for the safe-navigation-operator (?.) when the resulting value is an object.  The data will only be rendered once the object is defined so we don’t need as much null handling while waiting for data to load.

Finally, we can also implement another helpful ngIf feature by providing an alternative template to display when the data has not yet loaded.  We can do this by creating an ng-template and giving the ngIf directive a reference to the template as the else statement.  The Content Projection vs. Inputs article discusses ng-template in more detail.  Below you can see we create a spinner template that we will show in place of each data element while the data is loading.  Once loaded, we will then show the async results.

<h3>Async Data Load With If</h3>
<div *ngIf="counter$ | async as result else spinner">Time since subscribe: {{result}}</div>
<div *ngIf="message$ | async as result else spinner">Promise result: {{result}}</div>
<ng-template #spinner>
    <div class="spinner-border"></div>
</ng-template>

Conclusion

As you can see, the async pipe – especially when coupled with ngIf – can provide a high level of functionality for a common UI pattern with very little code.  The async pipe provides a clean interface, automatically follows best practices, and works very well with other Angular functionality.  While there still are many times in which manually handling subscriptions can be easier or cleaner, you’ll find many times that the async pipe fits very nicely when using the Smart vs. Dumb Components design pattern.

Related Articles In This Series

About Intertech

Founded in 1991, Intertech delivers software development consulting to Fortune 500, Government, and Leading Technology institutions, along with real-world based corporate education services. Whether you are a company looking to partner with a team of technology leaders who provide solutions, mentor staff and add true business value, or a developer interested in working for a company that invests in its employees, we’d like to meet you. Learn more about us.