Attack of the Async Pipe

by Stephen Fluin

2016-12-20

Attack of the Async Pipe
When building any sort Angular application that has a lot of asynchronous data flows, we sometimes find ourselves writing code that looks like this:
<div>
    {{ (data | async)?.parameter }}
</div>

We do this because we have some sort of observable that we just want to fetch a single property from each response. This can lead over time to us writing more and more of these:
<div>
    {{ (data | async)?.parameter }}
    {{ (data | async)?.otherParam }}
    {{ (data | async)?.otherParam2 }}
</div>

This quickly becomes more and more difficult to write, but it also creates multiple subscriptions on the underlying observable. Fortunately there are a few ways of avoiding this anti-pattern, but they each come with different trade offs and costs.

Parameter Observables

Instead of having one observable that we repeatedly subscribe to, we could make an observable for each parameter;
parameter = data.map(item => item.parameter);
otherParam = data.map(item => item.otherParam);
otherParam2 = data.map(item => item.otherParam2);

Pros:
  • This works well when there's exactly one copy of this data and gives us a cleaner template.

Cons:
  • This strategy isn't going to work within an ngFor.
  • This doesn't eliminate all of the asyncs
  • We still have multiple subscriptions

Smart Components / Dumb Components

A cleaner way of unrolling this problem is to use what are called Smart and Dumb components, or Presentation and Container Components. To take advantage of this strategy, we will use a parent component to handle the async subscription, and we will provide a child component the static information.
In the Smart component we have an async pipe.
<data-view [data]="data | async"></data>

In the Dumb component we take the data in as an@Input property, and then we can refer to all of the parameters directly.
<div>
    {{ data.parameter }}
    {{ data.otherParam }}
    {{ data.otherParam2 }}
</div>

Pros:
  • Single subscription to data object

Cons:
  • Requires a separation between the stream definition and the rendering, which feels a little bit like the old Angular 1.x Controller and Scope.

Magic Array Pipe

Pipes are a very powerful part of Angular. One strategy I've used is to combine the ability for *ngFor to iterate over an array with a local variable with our ability to turn anything into an array of one.
To take advantage of this, we must define a pure pipe that transforms any object it's given into an array:
@Pipe({ name: 'array' })
export class ArrayPipe implements PipeTransform {
    constructor() { }

    transform(value: any, ...args: any[]): any {
        if (value) {
            return [value];
        } else {
            return [];
        }
    }
}

This then allows us to do this in our template:
<div *ngFor="let dataPoint of data | async">
    {{ dataPoint.parameter }}
    {{ dataPoint.otherParam }}
    {{ dataPoint.otherParam2 }}

Pros
  • This should work whether data is able to resolve or not, which should help handle error cases, like nulls and undefineds
  • You could extend your array pipe to also solve the common problem of iterating over the keys found in maps / objects.

Cons:
  • Harder to intuit what's going on due to the use of an ng-for

Coming Soon: ngIf with local assignment

Take a look at Misko Hevery's commit that adds local assignment.
<div *ngIf="userObservable | async; else loading; let user">
  Hello {{user.last}}, {{user.first}}!
</div>

This technique is probably the cleanest, giving us the best of all worlds. This has landed in Angular's master repository, and will be available in the next major release, expected in February.
Pros
  • A single subscription regardless of how much data we want
  • Doesn't require manual or controller mapping
  • Will work in an *ngFor