The Importance of the Async Pipe
IMPORTANT: This lesson is still important for understanding Angular
applications that do not use signals. However, in applications that do use
signals there isn’t really any reason to use the async
pipe. Instead, you
would just convert your observable stream to a signal using toSignal
instead
— this achieves practically the same goal as using the async
, but has less
“gotchas” in terms of accidentally triggering unintended subscriptions. Many of
the motivations of using the async
pipe still overlap with the idea of using
toSignal
— the overall goal is still to keep your data in a reactive mechanism
until it reaches it’s destination.
The rest of this lesson will assume that the application is not using
signals. However, as you will see in the example applications we will be
building soon, we will not use the async
pipe. There are many references
to OnPush
change detection in this lesson which we will also not be using in
an application that uses signals.
We have seen a little of the async
pipe already in examples like this:
@Component({
selector: 'app-home',
template: `
<app-greeting [name]="name$ | async"></app-greeting>
`
})
export class HomeComponent {
name$ = of('Josh')
}
We can use it to pull values out of streams and pass them into inputs as above. We can also use it render out values directly in the template:
@Component({
selector: 'app-home',
template: `
<h2 *ngIf="name$ | async as name">{{ name }}</h2>
`
})
export class HomeComponent {
name$ = of('Josh')
}
Or, we can take this a little further and create a single view model vm$
stream that contains all of the data from streams for our entire template:
@Component({
selector: 'app-home',
template: `
<ng-container *ngIf="vm$ | async as vm">
<p>{{ vm.name }}</p>
<p>{{ vm.luckyNumber }}</p>
<p>{{ vm.attempt }}</p>
</ng-container>
`
})
export class HomeComponent {
name$ = of('Josh');
luckyNumber$ = from([5, 22, 587]);
attempts$ = new BehaviorSubject(0);
vm$ = combineLatest([name$, luckyNumber$, attempts$]).pipe(
map(([name, luckyNumber, attempt]) => ({name, luckyNumber, attempt}))
)
}
An important part about the *ngIf
syntax above is that this section of the
template won’t render until the stream emits and our vm
object is actually
available to use.
There are three main reasons for using the | async
pipe, and we will talk about each separately:
- The
async
pipe handles automatically subscribing and unsubscribing for you - Once we
subscribe
to a stream we are no longer coding “reactively”. Using theasync
pipe encourages us to keep data in streams until they reach their final destination (the template) - When a new value is emitted using the
async
pipe it will trigger change detection, which is important forOnPush
change detection
Let’s focus on these points in a little more detail.
The Importance of Unsubscribing
As we have seen, observables can emit multiple values over time. They will just
keep emitting these values, and triggering the next
handler of the observer,
until:
- It is unsubscribed
- It errors
- It triggers the
complete
notifier
Some observables might just emit one value, some might emit multiple then complete, some might emit values forever and never complete.
This creates the perfect situation for memory leaks. It creates the potential for subscribing to an observable that keeps emitting values over time and not unsubscribing from that without realising that it is still emitting values and doing things. Then maybe the user navigates to the component creating the subscription again, and again, and again, and so on.