Make TrackBy Easy to Use!
Tags: angular
Published: 2023-07-21 09:37:00

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.

```ts
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.

```ts
@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:

```ts
<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:

```ts
<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}](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lrwwkexkoqljip8lzz5l.png)

#### 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.

```ts
<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:

```ts
<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:

```ts
@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:

```ts
@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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/53r59f6mkpgq6o133qdo.png)

## Simplify imports

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

```ts
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 [Twitter](https://twitter.com/laforge_toma) or [Github](https://github.com/tomalaforge).Don't hesitate to reach out to me if you have any questions.