Using TrackBy With NgFor Loops In Angular

The use of TrackBy in my opinion is severely underrated in Angular. The performance benefits gained from using it can be huge (And very noticeable) for such a small code change. But before we dive too deep, let’s talk about how an NgFor loop works in Angular when you don’t use TrackBy at all.

Imagine a simple component with the following code behind :

export class AppComponent {
  employees : any[];

  resetEmployees() {
    this.employees = [
      {id : 1, name : 'John Smith'}, 
      {id : 2, name : 'Jane Smith'}, 
      {id : 3, name : 'Joe Bloggs'}, 
      {id : 4, name : 'Janey Blogs'}, 
      {id : 5, name : 'John Doe'}, 
      {id : 6, name : 'Jane Doe'}, 
    ];
  }
}

And a view that is also rather simple like so :

<button (click)="resetEmployees()">Reset</button>
<table>
  <tr *ngFor="let employee of employees">
    <td [attr.data-id]="employee.id">{{employee.name}}</td>
  </tr>
</table>

Now imagine a scenario like so :

  1. The page is loaded, no employees have been loaded, so the list is empty.
  2. We hit the reset button, and employees are set, and so the ngFor loop kicks off and draws them.
  3. We hit the reset button *again*. Employees are set, but it’s the same list! Doesn’t matter, ngFor loop kicks off and we re-draw them

It’s step 3 that is a problem. It’s an issue because drawing in the Dom is actually an expensive operation. Setting the list (even to the same list), means we remove all 6 items from the list, and then draw 6 items again.

So why?

The reason is that ngFor actually has a “default” trackBy. And it’s by object reference. Each time we set the list, because the actual reference has changed (Even the though the values have not), we redraw the entire table Dom again.

You can actually test this by viewing the elements in chrome dev tools. Each time you click reset, you’ll see the table light up like a xmas tree because it’s having to redraw everything.

Adding TrackBy

The first thing we need to do to add a TrackBy is to first identify a unique field on our array items. It actually doesn’t have to be a single field, but it should be some way to uniquely identify each record. In our case that’s easy, we can use the id field which is unique among all employees.

We then add a method that will take an item, and return the unique identifier.

getEmployeeId(index : number, employee : any) {
  return employee.id;
}

Then on our ngFor loop, we add the trackBy like so :

<table>
  <tr *ngFor="let employee of employees;trackBy:getEmployeeId">
    <td [attr.data-id]="employee.id">{{employee.name}}</td>
  </tr>
</table>

Note we don’t *call* getEmployeeId, we just pass the name of the method that we want to run to identify unique rows. Now when we run the reset method, and we watch it in Chrome Tools, we can see it no longer lights up as it’s not having to re-draw everything time and time again.

Where Is This Useful?

So the main question probably becomes, but where is this useful? You might think at first that you never do this sort of wholesale array setting. But in reality, you probably do. Imagine this code :

this.employeeService.getAll().subscribe(x => {
  this.employees = x;
});

It’s extremely common for API calls (Or really any rxJS subscription), to completely reset the value of an array with the results. If we didn’t use trackBy, and this ran often, we would end up redrawing a lot of elements that even if at first with a small number of results we didn’t notice, could quickly turn into a complete performance sink.

Other Notes On TrackBy

Trackby doesn’t suddenly make your code unresponsive. I’ve seen this crop up a little bit where people think if an item is set with the same id, then nothing about that item will update in the table, that’s not true.

For example if I created a method to change the name of an employee :

changeName() {
  this.employees[0].name = 'New Name';
}

And then drive it off a button click :

<button (click)="changeName()">Change Name</button>

The name is updated just fine. Trackby is about redrawing the overall list Dom, not the individual elements in it.

Same goes if we add an item like so :

addEmployee() {
  this.employees.push(
    {
      id : 7, 
      name : 'New Employee'
    }
  )
}

And trigger it from a button click. Again, it’s just as responsive with a trackBy than without.

Leave a Reply

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