Angular - Observable with async pipe used multiple times in template... Good Practice or Bad?
Asked Answered
H

7

44

If I have the need to bind multiple properties from the same observable within my component template...

For example:

<my-random-component[id]="(myObservable$ | async).id">
...
<my-random-component2[name]="(myObservable$ | async).name">

...am I better off doing it like I have above (which I see a lot), or is it more efficient to subscribe to my observable inside my .ts file, set a single object variable, and then bind to that? The idea with the latter approach being that the observable will only be called once.

Questions:

  1. Does the observable in the above code get called each time it is used via | async?
  2. Does the compiler do any efficiency magic behind the scenes to only call the observable once even if used 10 times w/in my template?
  3. Which approach is better/preferred?

Thanks!

Holystone answered 10/10, 2018 at 22:30 Comment(2)
This will be probably closed as opinion based but I am curious too :)Miche
Unfortunate... I truly thought it was a good question. They are either the same, or one is way more efficient (I would have thought).Holystone
C
38

Using the async pipe makes handling subscriptions much easier. It automatically handles unsubscribing unlike subscribing in the component.

That said, there is a better pattern than what the example is showing. Rather than having multiple async calls on components, you can write it 2 different ways. I'm assuming these components are in the same template file:

    <div *ngIf="(myObservable$ | async) as myObservable">
      <my-random-component [id]="myObservable.id">
      <my-random-component2 [name]="myObservable.name">
    </div>

Wrapping the code in ngIf does 2 things:

  • It cuts down on duplicate code
  • The components do not exist until myObservable$ is ready

There's also one more idea if you want to stick with calling async every single time:

    // COMPONENT
    name$: Observable<string>;
    id$: Observable<string>;
    
    ngOnInit() {
        // Return the exact value you want rather than the full object
    
        this.name$ = OBSERVABLE_SOURCE
        .pipe(
            map(res => res.name)
        );
    
        this.id$ = OBSERVABLE_SOURCE
        .pipe(
            map(res => res.id)
        );
    }
    // TEMPLATE
    <my-random-component [id]="(id$ | async)">
    <my-random-component2 [name]="(name$ | async)">

Pipes do not automatically run without a subscription. You can map, tap, or do anything else you want with it and it will not run until you add async/.subscribe().

Captor answered 11/10, 2018 at 1:0 Comment(2)
These are some good ideas, thanks for taking the time to respond. The first option had not occurred to me. I still wonder if simply going with a single subscription is the better way to go. I really don't mind the sub/unsub 'pattern'.Holystone
In the 2nd approach does API will be made 2 times or once?Piane
A
28

If you have multiple observables, you could wrap your entire page in a div that collects all the observables into a data object and then use them as needed :

<div *ngIf="{
  observable1: myObservable1$ | async,
  observable2: myObservable2$ | async
} as data">
  ... page content
  {{data.observable1.id}}: {{data.observable1.name}}

  {{data.observable2.status}}

</div>

Note: the *ngIf="{ ... }" is always true.

Credit goes to: https://medium.com/@ofirrifo/extract-multiple-observables-with-the-async-pipe-in-angular-b119d22f8e05

Anthropology answered 7/1, 2020 at 13:12 Comment(2)
I find this is a good way to start even with one observable since you tend to end up with multiple observables before you know it and then you don't need to refactor the first observable useAnthropology
Won't this lead to performance issues causing unnecessary change detection cycles? For instance, if my template is pretty large and inside data I also keep 10 more observables. Then, at some point, observable10 emits a new value. Won't this negatively affect all the rest consuming from 9 other observables?Cusick
B
19

You can just use share() to use the same observable and call it multiple times from html. Like this:

this.myObservable$ = this.anotherObservable$.pipe(share());

Then no matter how many times you call the observable from the HTML, it is called only once.

Berghoff answered 2/2, 2021 at 4:15 Comment(5)
I managed it to work using sharedReplay(). Don't ask me why 😄Hebraist
Our code attempted to use this method, where we used the same observable to pass down to multiple child components. It seems the first component in the HTML was the only one to receive the data though, despite .pipe(share()); being used. Probably, we were using it wrong. :) We just ended up refactoring the async pipe up into an <ng-container> parent so we only subscribed once.Dement
@AlexandreAnnic shareReplay() to be exact. Works for me too.Geezer
This is by far the clearest solution. All others either you have to use a *ngIf directive which is misleading, or create your own directive to make it work. Only downside I see is that it will not work with promises, but you can wrap it into an Observable.Agnail
using share will create another subject to make it hot and if you don't set the config properly for your use case, it can cause memory leak.Brogle
B
7

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.

Bevatron answered 13/8, 2022 at 16:5 Comment(0)
U
4

Another approach would be separating both structure and content rendering responsibilities without being bound to *ngIf.

You could use ng-template and ng-container together with context:

<ng-template #userTemplate let-user>
  <user-address [zipCode]="user?.zipCode"></user-address>
  <user-car [carCode]="user?.carCode"></user-car>
</ng-template>

<ng-container
  *ngTemplateOutlet="
    userTemplate;
    context: { $implicit: user$ | async }
  "
>
</ng-container>
Unboned answered 4/10, 2021 at 20:48 Comment(1)
As usual, the correct answer is near the bottom. Thank you!Downswing
N
1

A little bit late, but I think I can add something.

Concerning your questions...

1- The observable gets called each time you use the async pipe. If it sends a request to a server for instance, it will send it multiple times unless you use the shareReplay operator
2- The complier does not do anything behind the scenes as mentioned in 1

3- I think the best way to avoid that now is to use ngrx's LetDirective. All you have to do is:

  1. Add it to your project by running ng add @ngrx/component

  2. Import it in your module or standalone component:

     @NgModule({declarations: [LetDirective]}) class AppModule {}
    

    OR

     @Component({standalone: true, imports: [LetDirective]}) class MyComponenet {}
    
  3. In your HTML file, you can do the following:

      <ng-container *ngrxLet="myObservable$ as myData">
         <p>{{myData.property}}</p>
      </ng-container>
    
  • You can also alias your error and complete:
    ngrxLet="myObservable$ as myData; error as e; complete as c"
  • You don't have to use the ngrx store to use this directive. It's completely separate.
  • For more information about the LetDirective: NgRx Let Directive

However, if you don't prefer this technique, you can use this *ngif trick :

 <ng-container *ngIf="{myData: myObservable$ | async} as vm">
   <p>{{vm.myData.property}}</p>
 </ng-container>

  • This expression will always evaluate to true as it's an object. So, it will always be rendered
Norbertonorbie answered 25/12, 2023 at 15:15 Comment(0)
C
0

In case you're using ngrx, you could use the the Let Directive

https://ngrx.io/guide/component/let

//snippet from above mentioned doc:

<ng-container *ngrxLet="number$ as n">
  <app-number [number]="n"></app-number>
</ng-container>

I'm not sure if you can use this without having a whole ngrx store, haven't tried that yet.

Cobol answered 10/4, 2024 at 21:48 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.