Cutting Through the TrackBy Boilerplate
Anyone working with Angular has likely encountered the trackBy function within an *NgFor loop. If this is the first time you're hearing about it, there's still plenty of time to get up to speed.
The trackBy function provides Angular with a way to identify each item in an array, which ensures the DOM is updated correctly when the underlying array changes. In the absence of trackBy, Angular will tear down and recreate every DOM element. To keep your DOM from undergoing unnecessary re-renders during list mutations—whether you're adding, deleting, or reordering entries—you should make use of the trackBy function.
But here's the catch: wiring up this property to your NgFor directive entails a fair amount of boilerplate. You'd need to define a function that returns the identifying property for your list items and pass that function to the directive in your template.
interface Photo {
id: string;
url: string;
name: string;
}
@Component({
selector: 'list',
standalone: true,
imports: [NgFor],
template: `
<div *ngFor="let photo of photos; trackBy: trackById"> // 👈
{{ photo.name }}
<img [src]="photo.url" [alt]="photo.name" />
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ListComponent {
@Input() photos!: Photo[];
trackById(index: number, photo: Photo) { // 👈
return photo.id;
}
}
Streamlining the Setup
To trim down that boilerplate, we can craft a complementary directive that takes care of creating the trackById function for us.
@Directive({
selector: '[ngForTrackById]', // 1
standalone: true
})
export class NgForTrackByIdDirective<T extends { id: string | number }> {
@Input() ngForOf!: NgIterable<T>; // 2
private ngFor = inject(NgForOf<T>, { self: true }); // 3
constructor() {
this.ngFor.ngForTrackBy = (index: number, item: T) => item.id; // 4
}
}
Let's break down what each part is doing:
Step 1: The Selector Strategy
We're prefixing our directive's selector with ngFor. This lets us combine it directly with the NgFor directive like so:
<div *ngFor="let photo of photos; trackById"></div>
If you're not familiar with the shorthand for structural directives, the above is simply a more compact version of:
<ng-template ngFor let-photo [ngForOf]="photos" ngForTrackById"></ng-template>
Now it's clear why our directive needs that ngFor prefix 😇
Step 2: Input for Type Safety
The @Input isn't strictly necessary at runtime; its real value lies in type checking. We rely on it to capture the type of the array so that we can enforce robust type safety inside our directive. Should the Photo type lack an id property—and since our generic T is constrained to extend an object with an id—TypeScript will flag an error.
Remove the id property from the Photo type, and you'll be greeted with this error:
Step 3: Grabbing the NgFor Instance
The whole point of this directive is to assign the trackBy function on Angular's built-in NgForDirective. To do that, we need to access the current instance of NgFor. Since we know this directive is applied on the same view element, we set the self flag to true. This tells Angular to look only for the NgFor instance on that specific element.
<div *ngFor="let item of items; trackById">
// ☝️ ------------------------- 👈
<div *ngFor="let photo of photos; trackById">
// ☝️ ------------------------- 👈
</div>
</div>
If you try to use trackById outside the context of an NgFor directive, the ngFor property in your NgForTrackByIdDirective will be null, even in a setup like this:
<div *ngFor="let photo of photos">
<div ngFortrackById> // 👈 will not work
//
</div>
</div>
Important: If we specify no flags, or choose a different one like host, we'd get the instance from the element one level above in the example. But that's not the instance we're after.
Step 4: Assigning the TrackBy Function
Here, we instantiate the trackBy function for NgFor, telling it to track items based on the id of our Photo list.
The Payoff
With this in place, our code simplifies to:
@Component({
selector: 'list',
standalone: true,
imports: [NgFor, NgForTrackByIdDirective], // 👈
template: `
<div *ngFor="let photo of photos; trackById"> // 👈
{{ photo.name }}
<img [src]="photo.url" [alt]="photo.name" />
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ListComponent {
@Input() photos!: Photo[];
}
You might point out that this only works for objects with an id property. True enough. Which is why we can take it a step further and build a more generic directive that works with any property you choose.
Introducing NgForTrackByPropDirective
There's one small but crucial difference for this variant: we can't set the trackBy function inside the constructor, because it depends on an input property that hasn't been provided yet. The solution is to use a setter:
@Directive({
selector: '[ngForTrackByProp]',
standalone: true
})
export class NgForTrackByPropDirective<T> {
@Input() ngForOf!: NgIterable<T>;
@Input()
set ngForTrackByProp(ngForTrackBy: keyof T) { // setter
this.ngFor.ngForTrackBy = (index: number, item: T) => item[ngForTrackBy];
}
private ngFor = inject(NgForOf<T>, { self: true });
}
This directive maintains the same level of type safety as before.
Cleaning Up the Imports
As a final touch, we can tidy up the import array by bundling both directives alongside NgFor into a single module:
export const NgForTrackByDirective: Provider[] = [NgForTrackByIdDirective, NgForTrackByPropDirective];
@NgModule({
imports: [NgFor, NgForTrackByDirective],
exports: [NgFor, NgForTrackByDirective]
})
export class NgForTrackByModule {}
Now you're fully armed. There's no reason left to skip the trackBy function or leave it out because of the boilerplate that comes with it.
These two directives can be dropped straight into your project's source code with ease.
Go Ahead, Use Them! 🚀
You can find me on Twitter or Github. Don't hesitate to reach out to me if you have any questions.


