41

In my component template I am calling async pipe for same Observable in 2 places.

Shall I subscribe to it and use returned array in my template or using async pipe for same Observable in multiple places of template has no negative effect to performence?

Bogac
  • 3,596
  • 6
  • 35
  • 58
  • 3
    I assumed that using `|async` with the same observable within the same template only subscribes once but it seems this is not the case. It seems to be better to assign the result to a property and bind to that property instead. Still not sure. (Need to think of a use case that allows to verify) – Günter Zöchbauer Nov 22 '16 at 12:44

6 Answers6

134

Every use of observable$ | async will create a new subscription(and therefor an individual stream) to the given observable$ - if this observable contains parts with heavy calculations or rest-calls, those calculations and rest-calls are executed individually for each async - so yes - this can have performance implications.

However this is easily fixed by extending your observable$ with .share(), to have a shared stream among all subscribers and execute all those things just once for all subscribers. Don't forget to add the share-operator with import "rxjs/add/operator/share";

The reason why async-pipes don't share subscriptions by default is simply flexibility and ease of use: A simple .share() is much faster to write than creating a completely new stream, which would be required if they were to be shared by default.

Here is a quick example

@Component({
    selector: "some-comp",
    template: `
        Sub1: {{squareData$ | async}}<br>
        Sub2: {{squareData$ | async}}<br>
        Sub3: {{squareData$ | async}}
    `
})
export class SomeComponent {
    squareData$: Observable<string> = Observable.range(0, 10)
        .map(x => x * x)
        .do(x => console.log(`CalculationResult: ${x}`)
        .toArray()
        .map(squares => squares.join(", "))
        .share();  // remove this line and the console will log every result 3 times instead of 1
}
olsn
  • 16,644
  • 6
  • 59
  • 65
  • 4
    I wish I could give you a thousand upvotes for this!! Thanks – Grenville Feb 15 '17 at 12:25
  • This doesn't work for the two first values. When 0 is emitted, only `Sub1: {{squareData$ | async}}
    ` receives the element, when 1 is emitted only `Sub1` and `Sub2` receives the element
    – Olivier Boissé Dec 11 '17 at 16:02
  • @OlivierBoissé this is correct - see my comment on the 2nd answer: The question is not about the replay-feature or the syncronity of a stream - thus the answer also solely refers to the performance of async, regardless of the stream-type. – olsn Dec 12 '17 at 07:44
  • @olsn, thanks for this. Although I am using shareReplay() instead of share(), I think the same logic applies. I was originally appending the do(() => console.log(...)) AFTER the share(). That will log as many times as there are async pipes in your template. However, adding the console.log before the share() prints the expected number of times. I'm struggling to wrap my mind around this. Can you shed some light? Here is a link of someone else falling into the same trap as me https://github.com/angular-redux/store/issues/229#issuecomment-275082536 – Stephen Paul Jul 20 '18 at 13:10
  • That's totally expected: Any operator that you add to your stream, will essentially result in a completely new stream. So when you add an operator __after__ a `share` the newly resulted stream will only be shared until the point where the `share` was placed. – olsn Jul 20 '18 at 13:46
18

Another way of avoiding multiple subscriptions is to use a wrapping *ngIf="obs$ | async as someName". Using olsn's example

    @Component({
        selector: "some-comp",
        template: `
          <ng-container *ngIf="squareData$ | async as squareData">
            Sub1: {{squareData}}<br>
            Sub2: {{squareData}}<br>
            Sub3: {{squareData}}
          </ng-container>`
    })
    export class SomeComponent {
        squareData$: Observable<string> = Observable.range(0, 10)
            .map(x => x * x)
            .do(x => console.log(`CalculationResult: ${x}`)
            .toArray()
            .map(squares => squares.join(", "));

    }

It's also cool because it cleans out the template a bit too.

Hinrich
  • 13,485
  • 7
  • 43
  • 66
Roy Art
  • 578
  • 5
  • 10
  • this is the best answer for my case! – wbtubog Feb 01 '18 at 06:48
  • 3
    It's a cool solution in a lot of cases. But be careful if the observable returns a falsy value like "false", "0", ...! – Pierre Chavaroche Feb 25 '18 at 10:41
  • This is nice, but is there a way to do this without using a conditional? I always want to subscribe to the value once, but I'd also rather not put `| async` in half a dozen places. – Vala Sep 02 '19 at 09:02
  • You'd only have to use multiple `| async` if you have multiple different observables. Otherwise, simply put your _conditional_ wrapping the whole block. The alternative is simply doing this sort of thing in your controller; on your constructor/onInit and subscribe to the observable(s) and set the value of a non-observable value and use the non-observable in your template. – Roy Art Sep 03 '19 at 14:33
2

I had better luck with .shareReplay from 'rxjs/add/operator/shareReplay' which is very new (https://github.com/ReactiveX/rxjs/pull/2443)

I also had luck with .publishReplay.refCount(1) (Angular 2 + rxjs: async pipe with .share() operator)

I'm honestly not sure about the difference between the two strategies. The comments in the PR for shareReplay suggest that there might be more risk for memory leaks of Subscriptions if not implemented correctly, so I might go with the .publishReplay.refCount(1) for now.

squirrelsareduck
  • 874
  • 1
  • 10
  • 15
  • `share()` is just an alias for `publish().refCount()` - so what you did with `.publishReplay()` was adding a replay-feature to the stream in addition to sharing it, which does not really affect the scope of the question, since the question was just related to sharing&performance and not concerning any replay-capabilities of a stream. – olsn Jul 13 '17 at 08:37
2

Solution provided above by @Hinrich is very good however sometimes you are blocked because you want to use multiple observables in the template, in this case there is a simple solution like that (which work well for hot observables like a NgRx selector but maybe not well for a cold observable like http requests) :

@Component({
    selector: "some-comp",
    template: `
      <ng-container *ngIf="{ book: squareData$ | async, user: otherData$ | async } as data">
        Sub1: {{data.squareData}}<br>
        Sub2: {{data.otherData}}<br>
      </ng-container>
    `
})
Audwin Oyong
  • 2,247
  • 3
  • 15
  • 32
pegaltier
  • 494
  • 4
  • 11
  • Nice Solution. Just one question: if just one of the async pipes changes value, does that mean that everything gets rerendered ? Is the same data object reused, or is it a new instance every time book or user changes ? – bvdb Jun 01 '20 at 00:02
0

We use the @Hinrich solution but instead of the @pegaltier solution for multiple Observables, we use combineLatest().

this.data$ = combineLatest(book$, user$)
  .pipe(
    map(([book, user]) => {
      return (book && user) ? { book, user } : undefined;
    }),
  );
<ng-container *ngIf="data$ | async as data">
  {{ data.book }} {{ data.user }}
</ng-container>

jdforsythe
  • 1,057
  • 12
  • 22
0

You can make it like this, and in the template use the local variable

products$ = this._httpClient.get<Product[]>(`${this._environment.apiProducts}/product`)
.pipe(tap(data => console.table(data)),
  share()
);

// Template

  <ng-container *ngIf="products$ | async as products; else loading">
<div *ngFor="let product of products"></...>
<div *ngIf="products..."></...>
</ng-container>

I hope this post solves your problem

NsdHSO
  • 124
  • 7