変更通知を受信できないオブジェクトは差分検出してプロパティの変更を制御しよう

Angular でリストデータのバインディングは次のように書きます。

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <form>
      <div *ngFor="let person of people; let i = index">
        <label for="name{{i}}">名前:</label>
        <input type="text" id="name{{i}}" name="name{{i}}" [(ngModel)]="person.name">
        <label for="phone{{i}}">電話番号:</label>
        <input type="text" id="phone{{i}}" name="phone{{i}}" [(ngModel)]="person.phone">
      </div>
    </form>
  `,
})
export class AppComponent {
  readonly people = [
    { name: '山田太郎', phone: '03-1234-5678' },
    { name: '田中花子', phone: '06-9876-5432' },
  ];
}

ここで、people を次のように書き換えると無限に 'people get' が出力されてブラウザが死にます。

  get people(): { name: string; phone: string }[] {
    console.log('people get');
    return [
      { name: '山田太郎', phone: '03-1234-5678' },
      { name: '田中花子', phone: '06-9876-5432' },
    ];
  }

その詳しい理由は「Angular 変更検知」などで検索すると知る事ができますが、かいつまんで言うと、 「変更検出のために頻繁に people プロパティを参照(取得)しているが、取得の度にインスタンスが変更されているので再描画が行われる。」と理解しています。

なので、プロパティの getter で常にインスタンスを new するのはバッドパターンだと思います。

正攻法では、変更があった時だけ、people インスタンスを生成し直すことです。

  people = [
    { name: '山田太郎', phone: '03-1234-5678' },
    { name: '田中花子', phone: '06-9876-5432' },
  ];

  someYourFunc() {
    this.people = [
      { name: '斉藤静夫', phone: '07-8746-1234' },
    ];
  }

しかし複雑なデータ構造を持つデータの子や孫が変更された場合(例:伝票オブジェクトの品目群が変わった時)は、上記のような方法を取るために各所に仕組みを施す必要があります。

そこで、あまり推奨されないと思いますが、次のような方法もあります。

  @Input() data: { people: { name: string, phone: string}[] };

  private _people = [];
  get people(): { name: string; phone: string }[] {
    if (deepEquals(_people, this.data.people) {
      return _people;
    }
      
    console.log('people get');
    this._people = _.cloneDeep(this.data?.source ?? []);
    return this._people;
  }

表示したい people は data の子となっており、このコンポーネントではその変更通知を受信できないものとします。 このような場合だと、get people() { return this.data.people; } のようなプロパティを書いてしまいがちですが、people のインスタンス入れ替えタイミングが制御下にないので、それが頻繁だと冒頭のような事が起こる可能性を除去できません。

このコードではそれを防ぐために、_peopledata.people で差異がある場合にのみ、新しいインスタンスを生成して返却します。 肝心の差異の検出は deepEquals として省略していますが、要件次第でリスト件数だけを見れば良いケースもあれば、更新日時のような項目を比較する場合もあるでしょう。

これにより無限に 'people get' が出力されることは防ぐことができますが、「あまり推奨されない」を繰り返しておきます。 このケースでは data.people が変更された時に変更通知を送信し、このコンポーネントでそれを受信したことをトリガーにプロパティを更新すべきでそれが最適です。

この推奨されない方法では、people の getter が頻繁に呼び出されることに変わりはなく、deepEquals にコストがかかる場合にはやはりパフォーマンス低下の要因になりかねません。

結局、どんなUIフレームワーク、「変更通知の送信→受信」か「オンデマンドで差分検出」かの2択を迫られることになりますが、最適なのは前者と考えます。後者を採用する場合は用法用量を守って、何もしないよりマシと忘れずに。