noxi雑記

.NET、Angularまわりの小ネタブログ

ngForとtrackBy

Angular の *ngFor は配列をループして表示するために使用されるディレクティブです。 *ngFor ディレクティブには trackBy というオブジェクト追跡用のプロパティがあり、これを使用すると配列内のオブジェクトの位置変更を追跡することができます。この trackBy を色々試してみたためメモを残しておきます。



環境

Angular CLI: 9.1.7
Node: 12.16.2
OS: win32 x64

Angular: 9.1.9
... animations, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, router
Ivy Workspace: Yes

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.901.7
@angular-devkit/build-angular     0.901.7
@angular-devkit/build-optimizer   0.901.7
@angular-devkit/build-webpack     0.901.7
@angular-devkit/core              9.1.7
@angular-devkit/schematics        9.1.7
@angular/cli                      9.1.7
@ngtools/webpack                  9.1.7
@schematics/angular               9.1.7
@schematics/update                0.901.7
rxjs                              6.5.5
typescript                        3.8.3
webpack                           4.42.01

使い方

まずは trackBy の使い方を簡単に使ってみます。
trackByシグネチャーは Array の一般的なメソッド( mapfilter など)とは引数逆で (index: number, item: T): any になっています。覚えづらそうなことだけ覚えていれば思い出せそうです。また TrackByFunction<T> というインターフェースとして定義されていますので、これを型として使うのも良いでしょう。

それでは使ってみます。 TypeScript 側に trackBy に設定するための trackByNumber というプロパティを実装してみた一例です。今回は配列要素が Number のため、そのまま返すだけのシンプルなものになっています。

import { ChangeDetectionStrategy, Component, TrackByFunction } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {

  items: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

  /**
   * *ngForのtrackBy
   */
  trackByNumber: TrackByFunction<number> = (index: number, item: number): any => item;

}

そしてHTML テンプレート側です。 *ngFor のダブルクオーテーションの中、セミコロンで区切りを入れてから trackBy: trackByNumber を追加します。

<div *ngFor="let item of items; trackBy: trackByNumber">
  <span>{{ item }}</span>
</div>

trackBy使用有無のレンダリング差異

続いて trackBy を使用した場合と使用していない場合で、 Angular がどのようにレンダリングを変化させるのかについてです。 Primitive と Object についてそれぞれ検証してみました。結論は以下のようにインスタンスが同一かどうかで挙動が変化します。

  • 対象が Primitive の場合、要素の位置が変化しても trackBy 有無に関わらず Component の破棄・再生成・値の変更は行われない
  • 対象が Object の場合、要素の位置が変化してもインスタンスが同一なら trackBy 有無に関わらず Component の破棄・再生成・値の変更は行われない
  • 対象が Object の場合、インスタンスが異なると trackBy が存在しない場合 Component の破棄・再生成が行われる

昨今は Immutable なオブジェクトを使用することが多いですので、パフォーマンスや謎挙動防止に trackBy を積極的に使用していくのが良いかもしれません。