The Importance of the Async Pipe
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
asyncpipe handles automatically subscribing and unsubscribing for you - Once we
subscribeto a stream we are no longer coding “reactively”. Using theasyncpipe encourages us to keep data in streams until they reach their final destination (the template) - When a new value is emitted using the
asyncpipe it will trigger change detection, which is important forOnPushchange 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
completenotifier
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.
This creates a situation where, without us knowing, there is more and more observable subscriptions running in the background slowly consuming our applications memory.
The situations in which not explicitly unsubscribing from an observable is a problem vary — technically, in some situations, it doesn’t really matter. But, it is easy to mistake these situations, and in my opinion it’s not worth dedicating brain power to figuring out which scenarios are safe and which are not.
In general, I recommend always unsubscribing from observable streams when you are done with them. It is better to unsubscribe in a situation where maybe you didn’t technically have to, than to not unsubscribe in a situation where you should have.
This is what is great about the async pipe. If we use the async pipe, then
Angular handles unsubscribing from the stream for us automatically when the
component is destroyed. If we manually subscribe ourselves (e.g. in the
components class) then we also need to make sure we manually unsubscribe
ourselves as well.
The Importance of Keeping Data in Streams
We already covered in the last lesson the main reasons we want to keep data in streams:
- It gives us the ability to use the power of the RxJS operators
- It gives us the power to compose streams together
- It leads to a more declarative style of code
As we also discussed in the last lesson, when we subscribe to a stream we are no longer coding “reactively” — we are essentially losing all of these benefits.
However, we do need to subscribe at some point if we ever want to use the
values from the stream. By trying to always use the async pipe wherever we
can, we ensure that this happens at the last possible moment: when the data is
actually being displayed in the template.
Change Detection
We have already discussed change detection when using the OnPush strategy in
the basics section, but let’s quickly recap the scenarios in which change
detection will be triggered with OnPush:
- A components event handler is triggered inside of the component, e.g:
<button (click)="changeName()">Change name</button>- A components inputs have changed, e.g:
<some-component [someInput]="someValue"></some-component>- The
asyncpipe is used in the template and the stream/promise it is being used on emits a value:
<some-component [someInput]="someStream$ | async"></some-component>Now let’s consider the example we just looked at:
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})) )Instead of the name$ stream just being created directly like this, maybe it
comes from a service as we have looked at in previous examples.
We are displaying this name$ in the template (courtesy of our vm$ stream).
Now let’s imagine the service emits a new value for name, and now we want to see
that change reflected in the template:
<ng-container *ngIf="vm$ | async as vm"> <p>{{ vm.name }}</p> <p>{{ vm.luckyNumber }}</p> <p>{{ vm.attempt }}</p></ng-container>If we are using OnPush change detection, as we always will be, will change
detection be triggered?
Consider if any of the three change detection scenarios will be triggered.
Click here to reveal solution
Solution
- A components event handler is triggered inside of the component? Nope!
- A components inputs have changed? Nope!
- The
asyncpipe is used in the template and the stream/promise it is being used on emits a value? Yes!
Our change detection is only being triggered because of the fact that we are
using the async pipe. If we were not using the async pipe here, in order to
get change detection to run we would either need to:
- Use the
ChangeDetectorRefto manually trigger change detection (not a great solution) - Restructure the application so that the
namevalue is passed as an input to a separate child component so that it satisfies condition (2) (this is actually a good solution, but we might not always want to do this)
If we always use the async pipe to subscribe to streams in the template, then
we can be confident change detection will be triggered.
The Exceptions
There are always exceptions to the rules. I think it is extremely important in programming not to be dogmatic — that is to say don’t follow the rules just because they are “the rules”. In some sense, this is fine to do, especially as a beginner. If you are a beginner in a specific area (even if you are not a beginner generally) you probably don’t know why the rules are the rules, so it is generally safer to just follow them until you learn the why.
But you should still question why things are done a certain way, and sometimes
it makes sense to break these “rules”. If you are a beginner to RxJS and
reactive programming, I would recommend going to great lengths to avoid
subscribing manually and instead rely on the async pipe wherever possible.
Mostly because you just might not know the correct “pattern” to use to achieve
a certain thing with the async pipe yet. If you just assume that it’s one of
those situations where it’s not possible to use the async pipe and subscribe
manually, you might be missing out on learning how to architect your application
in a more reactive/declarative way. This will push you to investigate how to do
something reactively and learn new patterns, even if it sometimes leads you to
a dead end and manually subscribing is the correct approach.
As you gain experience, and start to understand better how to structure things “reactively”, the situations in which a manual subscribe might be the best solution will become more apparent. Then you can just use the manual subscribe right away without trying to jump through hoops to avoid a manual subscription.
In the next lesson, we will look at how to handle situations where you need to manually subscribe.