Subscription handling can be a hard task, so I will try to explain the whole scenario to you.
AsyncPype
Let's begin with some information about the AsyncPipe
.
Whenever you do an observableLike$ | async
, you are creating a subscription to the Observable, so your component's template will be re-render every time a new value is emitted. This is a very useful mechanism because Angular handles with the unsubscribe task when the component is destroyed. This is the description provided by Angular Documentation:
The async pipe subscribes to an Observable or Promise and returns the latest value it has emitted. [...] When the component gets destroyed, the async pipe unsubscribes automatically to avoid potential memory leaks. [...]
Problem
That said, we can answer your first question. In your example, it's not that the observable is being called multiple times, is your component that is creating and maintaining two different subscriptions of the same observable. You can verify that this is true by placing a tap
operator with a console.log()
in the observable's pipe.
If your Observable does an HTTP request, for example, it will do so as many times as you | async
it. (Obsviously, it's a bad practice to make an HTTP request like this, but you get the idea...)
In practice, you are creating two subscriptions to get parts of the value emitted, one in each, so there is no "efficiency magic behind the scenes to call the observable just once". In a perfect world, this should be avoided.
<my-random-component [id]="(myObservable$ | async).id">
<my-random-component2 [name]="(myObservable$ | async).name">
Possible Solution
A possible workaround for this problem is to use the *ngIf
structural directive and work with the template-context functionality of it. You make a | async
and give an "alias" for the value emitted, making just one subscription and accessing all of the attributes of the object.
<div *ngIf="(myObservable$ | async) as myObservable">
<my-random-component [id]="myObservable.id">
<my-random-component2 [name]="myObservable.name">
</div>
Possible Solution 2
Of course, you can always solve the *ngIf
problem with a ng-container
and a ng-template
, but that's a lot of boilerplate code for something that should be simple. This is too verbose to replicate across an entire system.
<ng-template #myTemplate let-myObservable>
<my-random-component [id]="myObservable.id">
<my-random-component2 [name]="myObservable.name">
</ng-template>
<ng-container *ngTemplateOutlet="myTemplate; context: { $implicit: myObservable$ | async }">
</ng-container>
Best Solution
Answering your last question, I personally think that the best solution is to create your own structural directive to handle these subscriptions created in the template.
You can isolate the *ngIf
template-context functionality and use it just to center the subscription, pretty much like a singleton pattern. It will be something like that:
<div *ngSub="myObservable$ as myObservable">
<my-random-component [id]="myObservable.id">
<my-random-component2 [name]="myObservable.name">
</div>
This behavior is the same as the previous solution, except you have a functionality that does only one thing. Oh, and note that you don't need to use an AsyncPipe
!
Because of type issues, it's better if you declare a let variable, rather than providing an alias for the observable, like this:
<div *ngSub="myObservable$; let myObservable">
<my-random-component [id]="myObservable.id">
<my-random-component2 [name]="myObservable.name">
</div>
You can check this Directive Implementation here (remember to give me a star lol), but basically, it takes an Observable, keeps a subscription of it, and passes all the values emitted via template context. It also unsubscribes whenever the component is destroyed. Take a look at this Angular 14 NPM package which contains this directive ready to use.