とんかつ時々あんどーなつ

〜たとえ低空でも飛行していられるように〜

@ngrx/component を使って zone-less Angular を試す

@ngrx/component が試せるようになっていたので使ってみた

サンプルコード

github.com

@ngrx/component とは

  • ドキュメントを見ると zone less を実現するためのヘルパーで Angular アプリケーションをより Reactive に書けるようになると書いてある
  • 試せる APILet DirectivePush Pipe の 2 つ

事前準備

  • 以下のようなサンプルアプリケーションを用意した(スタイル省略)
import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { BehaviorSubject, Observable, of } from 'rxjs';

@Component({
  selector: 'app-root',
  template: `
    <main>
      <h2>click event</h2>
      <section>
        <div>Message: {{ message$ | async }}</div>
        <button (click)="showMessage()">Click</button>
      </section>

      <h2>Http request</h2>
      <section>
        <ng-container *ngIf="user$ | async as user">
          <div>{{ user.name }}</div>
        </ng-container>
      </section>
    </main>
  `,
})
export class AppComponent implements OnInit {
  constructor(private http: HttpClient) {}

  messageSubject = new BehaviorSubject<string>('');
  user$: Observable<any | null> = of(null);

  get message$() {
    return this.messageSubject.asObservable();
  }

  ngOnInit() {
    this.fetchUser();
  }

  // NOTE: click event
  showMessage() {
    this.messageSubject.next('Hello world');
  }

  // NOTE: Http request
  fetchUser() {
    this.user$ = this.http.get('https://jsonplaceholder.typicode.com/users/1');
  }
}
  • サンプルアプリケーションには 2 つのシナリオを用意している
    1. ボタンクリックのイベントリスナー
    2. HTTP リクエストを介したデータの取得
  • Angular は通常 NgZone によってこれらを監視し、変更検知をトリガーしている
    • NgZone が変更検知を行っているわけではない
    • NgZone については公式ドキュメント を見るのがよい

zone-less

  • パフォーマンス向上のために NgZone を無効にすることもできる
  • しかし、NoopZone にすると変更検知のトリガーがされなくなり view の更新が行われなくなる
  • いよいよ @ngrx/component を使っていく

Let Directive

  • zone-full と zone-less のどちらのモードでも同じように動作する
  • *ngIf が falsy のときに描画されない問題も解消できる
    • observable な値を view にバインドするのに *ngIf が必要だったがそこの置き換えができる
  <section>
-   <ng-container *ngIf="user$ | async as user">
+   <ng-container *ngrxLet="user$ as user">
      <div>{{ user.name }}</div>
    </ng-container>
  </section>

Push Pipe

  • ngrxPush は AsyncPipe の代替であり zone-full と zone-less のどちらのモードでも同じように動作する
  • 以下のように置き換えると zone-less でも動作する
   <h2>click event</h2>
   <section>
-    <div>Message: {{ message$ | async }}</div>
+    <div>Message: {{ message$ | ngrxPush }}</div>
     <button (click)="showMessage()">Click</button>
   </section>

※ NOTE

  • 今回のサンプルコードだと async を ngrxPush に置き換えただけだと動作せず、下記のエラーが発生した
ERROR RangeError: Maximum call stack size exceeded
    at TapSubscriber._next (tap.js:43)
    at TapSubscriber.next (Subscriber.js:49)
    at MapSubscriber._next (map.js:35)
    at MapSubscriber.next (Subscriber.js:49)
    at DistinctUntilChangedSubscriber._next (distinctUntilChanged.js:50)
    at DistinctUntilChangedSubscriber.next (Subscriber.js:49)
    at Subject.next (Subject.js:39)
    at Object.next (component.js:263)
    at PushPipe.transform (component.js:389)
    at Module.ɵɵpipeBind1 (core.js:36650)
  • issue/2488 にもあったが asObservable() を使っているとテンプレートチェックのたびに新しい参照を返すために発生するとのこと
  • 今回は下記の変更も加えて回避した
   messageSubject = new BehaviorSubject<string>('');
+  _message$ = this.messageSubject.asObservable();
   user$: Observable<any | null> = of(null);
 
   get message$() {
-    return this.messageSubject.asObservable();
+    return this._message$;
   }
 
   ngOnInit() {

感想

ドキュメントにもまだ「実験的」な機能である旨の一文が書いてあるが、zone-less Angular を体験してみるにはとてもよかった。

実際のアプリケーションで zone を外すことを考えたことはまだないが、パフォーマンス懸念やデバッグの難しさから zone を外すことを検討しないといけなくなった場合に代替案があるのはいいと感じた。

参考