Three things you didn't know about the AsyncPipe
You sure heard about Angular’s AsyncPipe
haven’t you? It’s this handy little pipe that we can use from within our templates so that we don’t have to deal with unwrapping data from Observables or Promises imperatively. Turns out the AsyncPipe
is full of little wonders that may not be obvious at first sight. In this article we like to shed some light on the inner workings of this useful little tool.
Subscribing to long-lived Observables
Often when we think about the AsyncPipe
, we only think about values that resolve from some http call. We issue an http call, get an Observable<Response>
back, apply some transformations (e.g. map(...).filter(...)
) and finally expose an Observable to the template of our component. Here is what that typically looks like.
...
@Component({
...
template: `
<md-list>
<a md-list-item
*ngFor="let contact of contacts | async"
title="View {{contact.name}} details">
<img md-list-avatar [src]="contact.image" alt="Picture of {{contact.name}}">
<h3 md-line>{{contact.name}}</h3>
</a>
</md-list>`,
})
export class ContactsListComponent implements OnInit {
contacts: Observable<Array<Contact>>;
constructor(private contactsService: ContactsService) {}
ngOnInit () {
this.contacts = this.contactsService.getContacts();
}
}
In the described scenario our Observable is what we like to refer to as short-lived. The Observable emits exactly one value - an array of contacts in this case - and completes right after that. That’s the typical scenario when working with http and it’s basically the only scenario when working with Promises.
However, we can totally have Observables that emit multiple values. Think about working with websockets for instance. We may have an array that builds up over time! Let’s simulate an Observable that emits an array of numbers. But instead of emitting just a single array once, it will emit an array every time a new item was added. To not let the array grow infinitely we will limit it to the last five items.
...
@Component({
selector: 'my-app',
template: `
<ul>
<li *ngFor="let item of items | async">{{item}}</li>
</ul>`
})
export class AppComponent {
items = Observable.interval(100)
.scan((acc, cur)=>[cur, ...acc].slice(0, 5), []);
}
Notice how our list is kept nicely in sync without further ado thanks to the AsyncPipe
!
Keeping track of references
Let’s back of and refactor above code to what it would look like without the help of the AsyncPipe
. But while we’re at it, let’s introduce a button to restart generating numbers and pick a random background color for the elements each time we regenerate the sequence.
...
@Component({
selector: 'my-app',
template: `
<button (click)="newSeq()">New random sequence</button>
<ul>
<li [style.background-color]="item.color"
*ngFor="let item of items">{{item.num}}</li>
</ul>`
})
export class AppComponent {
items = [];
constructor () {
this.newSeq();
}
newSeq() {
// generate a random color
let color = '#' + Math.random().toString(16).slice(-6);
Observable.interval(1000)
.scan((acc, num)=>[{num, color }, ...acc].slice(0, 5), [])
.subscribe(items => this.items = items);
}
}
Let’s refactor the code to track subscriptions and tear down our long-lived Observable every time that we create a new one.
...
export class AppComponent {
...
subscription: Subscription;
newSeq() {
if (this.subscription) {
this.subscription.unsubscribe();
}
// generate a random color
let color = '#' + Math.random().toString(16).slice(-6);
this.subscription = Observable.interval(1000)
.scan((acc, num)=>[{num, color }, ...acc].slice(0, 5), [])
.subscribe(items => this.items = items);
}
}
Every time we subscribe to our Observable we save the subscription in an instance member of our component. Then, when we run newSeq
again we check if there’s a subscription that we need to call unsubscribe
on. That’s why we don’t see our list flipping between different colors anymore no matter how often we click the button.
Now meet the AsyncPipe
again. Let’s change the ngFor
again to apply the AsyncPipe
and get rid of all the manual bookkeeping.
@Component({
selector: 'my-app',
template: `
<button (click)="newSeq()">New random sequence</button>
<ul>
<li [style.background-color]="item.color"
*ngFor="let item of items | async">{{item.num}}</li>
</ul>`
})
export class AppComponent {
items: Observable<any>;
constructor () {
this.newSeq();
}
newSeq() {
// generate a random color
let color = '#' + Math.random().toString(16).slice(-6);
this.items = Observable.interval(1000)
.scan((acc, num)=>[{num, color }, ...acc].slice(0, 5), []);
}
}
I’m sure you’ve heard that the AsyncPipe
unsubscribes from Observables as soon as the component gets destroyed. But did you also know it unsubscribes as soon as the reference of the expression changes? That’s right, as soon as we assign a new Observable to this.items
the AsyncPipe
will automatically unsubscribe from the previous bound Observable! Not only does this make our code nice and clean, it’s protecting us from very subtle memory leaks.
Marking things for check
Alright. We have one last nifty AsyncPipe
feature for you! If you’ve read our article about Angular’s change detection you sure know that you can speed up Angular’s blazingly fast change detection even further using the OnPush
strategy. Let’s refactor our example and introduce a SeqComponent
to display the sequences while our root component will manage the data and pass it on via an input binding.
Let’s start creating the SeqComponent
which is pretty straight forward.
@Component({
selector: 'my-seq',
template: `
<ul>
<li [style.background-color]="item.color"
*ngFor="let item of items">{{item.num}}</li>
</ul>`
})
export class SeqComponent {
@Input()
items: Array<any>;
}
Notice the @Input()
decorator for items
which means the component will receive those from the outside via a property binding.
Our root component maintains an array seqs
and pushes new long-lived Observables into it with the click of a button. It uses an *ngFor
to pass each of these Observables on to a new SeqComponent
instance. Also notice that we are using the AsyncPipe
in our property binding expression ([items]="seq | async"
) to pass on the plain array instead of the Observable since that’s what the SeqComponent
expects.
@Component({
selector: 'my-app',
template: `
<button (click)="newSeq()">New random sequence</button>
<ul>
<my-seq *ngFor="let seq of seqs" [items]="seq | async"></my-seq>
</ul>`
})
export class AppComponent {
seqs = [];
constructor () {
this.newSeq();
}
newSeq() {
// generate a random color
let color = '#' + Math.random().toString(16).slice(-6);
this.seqs.push(Observable.interval(1000)
.scan((acc, num)=>[{num, color }, ...acc].slice(0, 5), []));
}
}
So far, we haven’t made any changes to the underlying change detection strategy. If you click the button a couple of times, notice how we get multiple lists that update independently at a different timing.
However, in terms of change detection, it means that all components are checked each time any of the Observables fire. That’s a waste of resources. We can do better by setting the change detection for our SeqComponent
to OnPush
which means it will only check it’s bindings if the inputs - the array in our case - changes.
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'my-seq',
...
})
That works and seems to be an easy quick win. But here comes the thing: It only works because our Observable creates an entirely new array each time it emits a new value. And even though that’s actually not too bad and in fact beneficial in most scenarios, let’s consider we use a different implementation which mutates the existing array rather than recreating it every time.
Observable.interval(1000)
.scan((acc, num)=>{
acc.splice(0, 0, {num, color});
if (acc.length > 5) {
acc.pop()
}
return acc;
}, [])
If we try that out OnPush
doesn’t seem to work anymore because the reference of items
simply won’t change anymore. In fact, when we try that out we see each list doesn’t grow beyond its first element.
Meet the AsyncPipe
again! Let’s change our SeqComponent
so that it takes an Observable instead of an array as its input.
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'my-seq',
template: `
<ul>
<li [style.background-color]="item.color"
*ngFor="let item of items | async">{{item.num}}</li>
</ul>`
})
export class SeqComponent {
@Input()
items: Observable<Array<any>>;
}
Also note that it now applies the AsyncPipe
in its template since it’s not dealing with a plain array anymore. Our AppComponent
needs to be changed as well to not apply the AsyncPipe
in the property binding anymore.
<ul>
<my-seq *ngFor="let seq of seqs" [items]="seq"></my-seq>
</ul>
Voila! That seems to work!
Let’s recap, our array instance doesn’t change nor does the instance of our Observable ever change. So why would OnPush
work in this case? The reason can be found in the source code of the AsyncPipe
itself
private _updateLatestValue(async: any, value: Object): void {
if (async === this._obj) {
this._latestValue = value;
this._ref.markForCheck();
}
}
The AsyncPipe
marks the ChangeDetectorRef
of our component to be checked, effectively telling the change detection that there may be a change in this component. If you like to get a more detailed understanding of how that works we recommend reading our in-depth change detection article.
Conclusion
We use to look at the AsyncPipe
as a nifty little tool to save a couple of lines of code in our components. In practice though, it hides a lot of complexity from us that comes with managing async tasks. It’s pure gold.