Make TrackBy Easy to Use!

achtlos

thomas

Posted on July 21, 2023

Make TrackBy Easy to Use!

If you are an Angular user, you must have heard about the trackByfunction 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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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[];
}
Enter fullscreen mode Exit fullscreen mode

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 });
}
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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 Twitter or Github.Don't hesitate to reach out to me if you have any questions.

💖 💪 🙅 🚩
achtlos
thomas

Posted on July 21, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related