Observable with async pipe used multiple times in Angular template
There is a question in stackoverlfow Angular - Observable with async pipe used multiple times in template… Good Practice or Bad?
The question is that using async pipe to subscribe the same observable like the following, is it good or bad.
<my-random-component[id]="(myObservable$ | async).id">
<my-random-component2[name]="(myObservable$ | async).name">
Observable is similar to function. If you don’t call a function, a function
will not use extra computer resource. If you don’t subscribe a observable,
observable will not use extra computer resource. Only subscribing will get
an observable executed. In the above example, myObservable$
is subscribed twice
by calling async
filter, so the observable is executed twice. If it is network
call, and the executions may be triger two network calls. And they are independent
of each other. Even worse, if two network calls can return different results.
In the following code, we can see two async pipe trigger two observable execution.
export class MainComponent {
names = [
"Tom",
"Jerry",
"Micky",
"Goofy",
"Dony",
"Cookie",
"Alisa",
"Elaine",
];
obs$ = new Observable((subscriber) => {
const n = Math.trunc(Math.random() * this.names.length);
const name = this.names[n];
console.log(`pretending fetching data over network, got ${name}`);
subscriber.next(name);
subscriber.complete();
});
}
<h2>multiple async pipe</h2>
<div>
<p>{{ obs$ | async }}</p>
<p>{{ obs$ | async }}</p>
</div>
The answer to this question in stackoverflow, is similar like the following.
<h2>single async pipe with `ngIf` directive</h2>
<div *ngIf="obs$ | async as name">
<p>{{ name }}</p>
<p>{{ name }}</p>
</div>
This solution works, however the ngIf
directive is not designed for this
purpose. If template inside is big, it can trigger lots of DOM update.
The following is part of the source code of ngIf
.
private _updateView() {
if (this._context.$implicit) {
if (!this._thenViewRef) {
this._viewContainer.clear();
this._elseViewRef = null;
if (this._thenTemplateRef) {
this._thenViewRef =
this._viewContainer.createEmbeddedView(this._thenTemplateRef, this._context);
}
}
} else {
if (!this._elseViewRef) {
this._viewContainer.clear();
this._thenViewRef = null;
if (this._elseTemplateRef) {
this._elseViewRef =
this._viewContainer.createEmbeddedView(this._elseTemplateRef, this._context);
}
}
}
}
The line this._viewContainer.clear();
means when the observable emit new
value, event it is truthy, the a big truck of DOM will still get destroyed and
rendered.
To solve this problem, I have create let
directive. This directive is purely
served to declare a template variable to store a value which can be
refernced in template. It is much lightweight than ngIf
. But the usage is
similar to ngIf
.
<h2>single async pipe with `let` directive</h2>
<div *let="(obs$ | async) as name">
<p>{{ name }}</p>
<p>{{ name }}</p>
</div>
The challenge part of it is the synatax support like
*let="(obs$ | async) as name"
, after checking the code of ngIf
, I found that
the context of the directive has to be something like this
export class LetContext<T = unknown> {
public $implicit: T = null!;
// need this to support syntax, `*let="(obs$ | async) as name"`
public let: T = null!;
}
updateView() {
// context.let == context.$implicit
const context = {
$implicit: this.let,
let: this.let,
};
if (this.embeddedView) {
this.embeddedView.context = context;
} else {
this.embeddedView = this.viewContainer.createEmbeddedView(
this.templateRef,
context
);
}
}
This let
directive can also store any value on the fly, you can store
a json literal to a variable only visible in template, you can also rename
component property, when the value of component change, the new variable
also get changed. Here is the sample code.
<h2>Store any value on the fly</h2>
<div *let="{first: 'John', last: 'Doe'} as person">
{{person.first}}, {{person.last}}
</div>
<h2>rename a component property</h2>
<div *let="greeting as message">{{message}}</div>
The whole source code is hosted as below.