trackBy Directive
When rendering lists in Angular, any change to the data array can cause Angular to re-render the entire DOM for that list. This can lead to performance degradation. To solve this, Angular provides a mechanism to uniquely identify each item, ensuring that only the changed elements are updated.
The Modern Angular Solution: @for and track
Starting from Angular v17, the new Control Flow syntax, @for, solves this problem very elegantly. It allows you to specify a unique property for tracking each item directly using the track keyword.
<!-- app.component.html -->
<ul>
@for (item of data(); track item.id) {
<li></li>
}
</ul>
The track item.id syntax completely eliminates the need for a trackBy function. If you are working on a new project or using a modern version of Angular, using @for with track is always the best approach.
When the value of an array bound to ngFor
changes, a refresh occurs, triggering re-rendering. To prevent this, we can leverage trackBy
. Essentially, using trackBy
can boost performance by preventing re-rendering even when values change.
Creating a trackBy Directive for the Legacy *ngFor
However, you might still need to use *ngFor in legacy projects or due to compatibility issues with certain libraries. To optimize performance with *ngFor, you must use the trackBy property along with a tracking function (trackByFn).
<!-- app.component.html -->
<div *ngFor="let item of data; trackBy: trackByFn"></div>
// app.component.ts
...
trackByFn = (index: number, item: any): number => item.id;
Drawbacks of trackBy
The main drawback of the conventional trackBy
approach is that you need to create a trackByFn
function within the component for every ngFor
loop. This inconvenience often leads developers to intentionally or accidentally omit trackBy
, which can negatively impact product performance.
Understanding trackBy
Behavior
To create a trackBy
Directive, let’s first understand how trackBy
works under the hood. We can gain more insight by converting the *ngFor
syntax to its ng-template
equivalent. The following two code snippets produce the same result:
<div *ngFor="let item of data; trackBy: trackByFn"></div>
<ng-template ngFor let-item [ngForOf]="data" [ngForTrackBy]="trackByFn"></ng-template>
As you can see from the ng-template
version, the original name of trackBy
is ngForTrackBy
, and it’s essentially an input binding of the ngFor
directive.
Challenges of Creating a trackBy
Directive
How can we turn ngForTrackBy
into a directive? A naive approach might look like this:
<div *ngFor="let item of data" [trackByFn]="'id'"></div>
Expanding this into its ng-template
form reveals the problem:
<ng-template ngFor let-item [ngForOf]="data">
<div [ngForTrackByFn]="'id'"></div>
</ng-template>
Using this approach, the custom directive ngForTrackByFn
is recognized as a separate directive, not an input binding of the ngFor
directive. Therefore, it cannot influence the ngFor
behavior, preventing us from achieving the desired functionality.
So, how can we access the ngFor
directive?
Creating an ngForTrackBy
Directive Using @Host
To address this, we introduce the @Host
decorator. @Host
is a built-in Angular feature that, similar to @Self
, allows us to retrieve associated dependency injections (DIs).
The key difference is that @Self
retrieves DIs associated with the component itself, while @Host
retrieves DIs associated with the component’s template and viewProviders
.
In our case, we can use @Host
to access the ngFor
directive and inject the desired field value into the ngForTrackBy
function belonging to ngFor
. We’ll specify NgForOf
in @Host
to ensure we’re accessing the ngFor
directive. Here’s the code:
// ng-for-track-by-field.directive.ts
import { NgForOf } from '@angular/common';
import { Directive, Input, inject } from '@angular/core';
@Directive({
// ngFor와 함께 사용될 때만 활성화되도록 셀렉터 구성
selector: '[ngFor][ngForTrackByField]',
standalone: true,
})
export class NgForTrackByFieldDirective<T extends Record<string, any>> {
@Input('ngForTrackByField')
public field!: keyof T;
// host 요소에 있는 NgForOf 인스턴스를 주입받습니다.
private ngFor = inject(NgForOf<T>, { host: true });
constructor() {
// NgForOf 디렉티브의 trackBy 함수를 동적으로 설정합니다.
this.ngFor.ngForTrackBy = (index: number, item: T) => {
// field가 지정되었다면 해당 프로퍼티 값을, 아니면 아이템 자체를 반환합니다.
return this.field ? item[this.field] : item;
};
}
}
In the code above, ngForTrackByField
is a directive that is hosted by ngFor
. When invoked, it injects the desired value into the ngForTrackBy
function of ngFor
and executes it.
Now, we can easily implement trackBy
in the template using trackByField
:
Using the Directive
Now, you can implement trackBy in your templates without needing a trackByFn in your component.
// app.component.ts
import { Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common'; // For *ngFor
import { NgForTrackByFieldDirective } from './ng-for-track-by-field.directive';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, NgForTrackByFieldDirective], // Import the directive
template: `
<h3>*ngFor with Custom trackBy Directive</h3>
<div *ngFor="let item of data(); ngForTrackByField: 'id'">
</div>
`,
})
export class AppComponent {
data = signal([ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' } ]);
// No more trackByFn needed here!
}
댓글남기기