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.