Make TrackBy Easy to Use!

If you are an Angular user, you must have heard about the trackBy function inside an *NgFor loop. If you have never heard of it, it’s not too late to learn about it.

The trackBy function lets Angular know how to identify items in an Array to refresh the DOM correctly when you update that array. Without trackBy , the entire DOM elements get deleted and added again. If you want to preserve your DOM from unnecessary re-rendering when adding, deleting or reordering list elements, use the trackBy function.

However adding this property to your NgFor directive requires a lot of boilerplate. You need to create a function that returns the property that identifies your list element 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;
}
}

Simplify the boilerplate

To simplify the boilerplate, we can create an additional directive that handles the instantiation of this trackById function.

@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 go though the code:

Line 1

We prefix our directive selector with ngFor , this way we can combine it with the NgFor directive like this:

<div *ngFor=”let photo of photos; trackById”></div>

If you are not familiar with the shorthand syntax of structural directive, the above is the simplification of:

<ng-template ngFor let-photo [ngForOf]=”photos” ngForTrackById”></ng-template>

We can now easily see why we need to prefix our directive with ngFor 😇

Line 2

The @Input is only useful for type checking. We need to obtain the type of the array to enforce strong type safety within our directive. If the type Photo doesn’t have an id property, and since the generic T extends id, we will get a Typescript error.

If we remove the id property from the type Photo, we will see the following error:

Typescript error because Photo doesn’t extends {id: string | number}

Line 3

The goal of the directive is to set the trackBy function of the built-in NgForDirective. Thus we need to access the current instance of the directive NgFor. Since we know that we are using the directive on the same VIEW element, we set the self flag to true. This means we are only going to look for the NgFor instance on this element.

<div *ngFor=”let item of items; trackById”>
// ☝️ ————————- 👈
<div *ngFor=”let photo of photos; trackById”>
// ☝️ ————————- 👈

</div>
</div>

If you want to use trackById outside an NgFor directive, the property ngFor inside your NgForTrackByIdDirective will be null even if you have something like this:

<div *ngFor=”let photo of photos”>
<div ngFortrackById> // 👈 will not work
//
</div>
</div>

Note: If we don’t set any flags, or use a different flag such as the host flag, we will obtain the instance of the line above in the example provided. However, that is not the instance we want to work with.

Line 4

We instantiate the trackBy function of NgFor to track the id of our Photo list.

Result

Now, our code becomes:

@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[];
}

But you may say this only works for objects with an id property. That’s true, which is why we can create a more general directive to accept any properties.

NgForTrackByPropDirective

The only small difference we need to apply to our directive is that we cannot set the trackBy function inside the constructor since it relies on an input property. To resolve this, we will create 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 is type safe as well.

Typescript error because Photo doesn’t have an other property

Simplify imports

Last but not least, we can simplify the import array by creating a module that imports both directive combined with NgFor

export const NgForTrackByDirective: Provider[] = [NgForTrackByIdDirective, NgForTrackByPropDirective];

@NgModule({
imports: [NgFor, NgForTrackByDirective],
exports: [NgFor, NgForTrackByDirective]
})
export class NgForTrackByModule {}

Now you are well-equipped and have no more excuses to forget the trackBy function or omit it due to boilerplate code.

Those two directives can be easily integrated into your project’s source code.

Enjoy using them! 🚀

You can find me on Medium, Twitter or Github. Don’t hesitate to reach out to me if you have any questions.

Make TrackBy Easy to Use! was originally published in ngconf on Medium, where people are continuing the conversation by highlighting and responding to this story.

Leave a Comment

Your email address will not be published. Required fields are marked *