47

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!

2
  • This will be probably closed as opinion based but I am curious too :) Commented Oct 10, 2018 at 23:53
  • 1
    Unfortunate... I truly thought it was a good question. They are either the same, or one is way more efficient (I would have thought). Commented Oct 11, 2018 at 0:42

8 Answers 8

40

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().

Sign up to request clarification or add additional context in comments.

2 Comments

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'.
In the 2nd approach does API will be made 2 times or once?
30

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

2 Comments

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 use
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?
23

Use share() to create a single observable that can be reused across multiple HTML calls. This way, no matter how many times it’s called in the template, the observable executes only once.

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

5 Comments

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.
@AlexandreAnnic shareReplay() to be exact. Works for me too.
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.
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.
@AlexandreAnnic - It's shareReplay not sharedReplay ;)
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.

Comments

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>

1 Comment

As usual, the correct answer is near the bottom. Thank you!
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

Comments

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.

Comments

0

Many already said - it may be inefficient or plain wrong to subscribe to the same Observable in the same template multiple times. The *ngIf is inconvenient and misleading, ngContainer and/or ngTemplates are an unnecessary boilerplate. But, unfortunately, there is no way around it in the Angular until... the version 18.1.0-next.4.

If you're using Angular ver >= 18.1.0-next.4, you can use @let syntax in your template. For example:

<section class="blog-wrapper">
    @let searchQuery = (searchQuery$ | async) || '';
    @if (searchQuery.length > 2) {
        <article>
            <div>Search results for "{{ searchQuery }}"</div>
            <div>Number of articles found: {{ ds.articlesTotal }}</div>
        </article>
    } @else if ((ds.articlesTotal || 0) > 0 && searchQuery.length <= 2) {
        <article>
            <div>Latest articles: Not looking because you typed only {{ searchQuery.length }} chars!!</div>
            <div>Number of articles found: {{ ds.articlesTotal }}</div>
        </article>
    }
    ... some other html ...
</section>

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.