Skip to content

Using trackBy with *ngFor

TrackBy Cover Image

As I’m writing this I’m listening to a Vaporwave Synthwave mix (I have a weird musical taste ranging from classical music to Japanese city-pop, but that is not important for this article). One thing that I noticed is that lists are everywhere! Comments on the video are list objects, recommended videos are list objects, categories too, and chapters/timestamps certainly flirt with this idea too. If you use any framework you’ll undoubtedly work with lists in one way or another. In Angular specifically, we are given a structural directive called *ngFor. Not wanting to reiterate what is already said about the directive, the most important thing to know is that *ngFor allows us to loop over arrays generating custom chunks of HTML for each iterated item.

Large lists, especially those with external functionalities such as editing and deleting can bog the application if not properly implemented. Why does this happen? Well, because *ngFor, as mentioned above creates an HTML context. for each element. Depending on the list provided in the *ngFor Angular will try to assume its unique identifier (most commonly the object itself). Not much of a problem if the identifier is a primitive property, as it won’t trigger a re-render. However, if the memory address changes, the HTML context will trigger a re-render. Simple things such as updating or deleting records could re-render the whole UI because the elements in the list are referenced by the object itself. The object changes the memory location, suddenly the app gets confused, and rebuilds the whole HTML context.

What is the solution for this? We could use any other unique identifier instead of an object; for example, in a list of customers it could be a property of the customer, such as an email or an id. In that case, even if a new object is returned for each element, Angular wouldn’t re-render all the items in the list, as it is smart to know that new objects have the same unique identifier.

Learning stuff always gets easier with an example, so let us write a simple project to utilize trackBy function.

trackBy example

Take a look at the mini-project below. I am using the digimon API because I felt nostalgic for one of my favorite cartoons of my childhood. We will ignore possible mistakes regarding type annotations and/or coding practices. The purpose of the project is to showcase how useful using trackBy can be. If you take a look at the data.service.ts you’ll notice that I purposefully provided a new object every time we remove a Digimon from the list. Because we are using the object reference as the unique identifier, every time the UI will re-render. (You can open the console in the developer tools to verify this).

What happens? We initially have 50 console logs, removing one we get 49, removing another one we get 48, and so on. Everytime we re-render the whole UI list, BUT we only remove one element at a time. It isn’t very optimized, which we could test by retrieveing 1000 records instead of only 50.

  getDigimon(pageSize = 50): Observable<Digimon[]> {
    if (this.digimonContent && this.digimonContent.length) {
      return of([...this.digimonContent]);
    }

    return this.http
      .get(`https://digimon-api.com/api/v1/digimon?pageSize=${pageSize}`)
      .pipe(
        map((digimonResponse: { content: Digimon[]; pageable: {} }) => {
          this.digimonContent = [...digimonResponse.content];
          return this.digimonContent;
        })
      );
  }

  deleteDigimon(deletedDigimon: Digimon) {
    this.digimonContent = this.digimonContent
      .filter((digimon) => {
        return digimon.name !== deletedDigimon.name;
      })
      .map((digimon) => ({ ...digimon }));
  }

Again, remember that we are using the spread operator { ... } to return a new object every time we delete a record. In a real-world scenario this wouldn’t be the best practice, but this is a testing playground. On your own projects, always use the coding practices you deem best.

How is this fixable? By simply adding the following code inside the HTML template and the component:

  <ng-container *ngFor="let digimon of digimonList; trackBy: trackByFn"
    ><app-digimon-list
      [digimon]="digimon"
      (digimonDeleted)="deleteDigimon($event)"
    >
    </app-digimon-list
  ></ng-container>
  trackByFn(_, digimon: Digimon) {
    return digimon.id;
  }

Now, we can again open the console, and voila, we do not re-render any UI! Angular now knows how to optimize defining items in the list. A thing to note here is that the trackBy function must be without any side effects, it should always return the same value for the same input, return unique values for all unique inputs and be fast enough.