Skip to content

Improving Angular ngFor using trackById Directive

Using ngFor in Angular

When you need to iterate over a collection in Angular, you will probably use the ngFor directive that will instantiate a template once per item from the collection.

If we need to change the data in the collection, for example as a result of an API request, we have a problem because Angular can’t keep track of items in the collection. It also has no knowledge of which items have been removed or added.

As a result, Angular needs to remove all the DOM elements associated with the data, and create them again. That means a lot of DOM manipulations especially in the case of a big collection. And as we know, DOM manipulations are expensive.

The Solution and why we should use trackBy every time.

We can help Angular to track which items added or removed by providing a trackBy function. The trackBy function takes the index and the current item as arguments, and returns the unique identifier for this item.

Let's start.

If you have written a production Angular app before, you probably are using trackBy feature to display a list of data, which looks something like:

interface Item {
  id: number;
  name: string;
}

@Component({
  selector: 'app-list',
  template: ` <div *ngFor="let item of items; trackBy: trackById">...</div> `,
})
export class AppListComponent {
  @Input()
  public items: Item[];

  public trackById(index: number, item: Item) {
    return item.id;
  }
}

More often than we want, we get the data from the backend, and the information identifying an item is always the same, such as a property named id. But the downside of using this is that Angular forces us to write a trackBy function such as the one described above in each component. This can become really annoying and less performant, because we are duplicating the function in each component with the same code.

What if I told you there is a way to only write a global directive once, and use it in every component you need?

A little hint would be:

...
<div *ngFor="let item of items; trackById"></div>
...

So how could we make the above work? First of all, we need to understand how the directive *ngFor works. The syntax used for ngFor is microsyntax, which is briefly documented:

The microsyntax parser takes of and trackby, title-cases them (of -> Of, trackBy -> TrackBy), and prefixes them with the directive’s attribute name (ngFor), yielding the names ngForOf and ngForTrackBy. Those are the names of two NgFor input properties . That’s how the directive learns that the list is heroes and the track-by function is trackById.

In other words, these blocks of code are equivalent, and in fact, are examples of how Angular translates the structural directive:

<div *ngFor="let item of items; trackBy: trackById"></div>

<div *ngFor="let item of items; trackById"></div>

Creating a custom TrackBy Directive

Now, going one step further, we can form a plan on how to implement out 'keyword'. We simply have to create a directive with a ngForTrackById input.

@Directive({
  selector: '[ngForTrackById]',
})
export class NgForTrackByIdDirective<T extends Item> {
  constructor(@Host() private ngFor: NgForOf<T>) {
    this.ngFor.ngForTrackBy = (index: number, item: T) => item.id;
  }
}

Let's talk about what we did on the previous block of code.

  • Using a T generic to refer a type which has an id field.
  • Using [ngForTrackById] as our selector, we explain how Angular simply translates the microsyntax into this directive.
  • Using the constructor(), we rely on it running before the ngForTrackBy input on the NgForOf directive.
  • Add the @Host() decorator because we 're only interested in the host element.
  • Then, we simply overwrite the function as we see inside the constructor.

Let's see it in action

Conclusion

In this short but helpful tutorial, we created a directive to improve our performance. We avoided using the same block of code repeatedly to trackBy in every *ngFor, and his proper method in the component. This implementation will help you have a more clear and useful trackBy. However, here we use trackBy as the field id, but the directive can be changed to support trackyBy in other fields, and make it more flexible.