Loops and Iterations
Angular Best Practices HTML SPA Typescript Visual Studio Code

How to use NgFor Effectively in an Angular Application

Welcome to today’s post.

In today’s post I will discuss how we can use for loops effectively in an Angular application component template.

I will show how to use un-tracked for-loops, tracked for-loop iterators, and how iterated items are rendered as HTML outputs within the DOM tree. I will show how the tracked iteration approach renders more effectively in the DOM. This is important in applications where effective rendering is required in forms where there are a high number of item iterations that can result in higher memory usage and application response. I will finally explain when we should and when we shouldn’t utilize tracked iterators.

I will demonstrate the above in the context of the use of the NgFor structural directive that is used to define an iteration loop within an Angular HTML template.

NgFor with non-Tracked Iterations

I will start off with a basic example that uses non-tracked iteration.

The most common pattern is within a component that takes a data source as an input parameter, then displays this data within the HTML template.

The example is shown below with the following HTML template:

<p>List of Books 1</p>

<li *ngFor="let item of items; index as i">
  <b>Book Entry:</b> {{ item.id }}, {{ item.title }}
</li>

<p>Reset Book Data</p>

<button (click)="resetBookData()">Reset Book Data</button>

The corresponding typescript code is shown below:

import { Component, VERSION } from '@angular/core';
import { Book } from './book';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  name = 'Angular ' + VERSION.major;
  items: Book[] = [];

  constructor() {
    for (var i = 0; i < 100; i++) {
      this.items.push({ id: i, title: 'book title ' + i });
    }
  }

  resetBookData() {
    this.items = [];
    for (var i = 0; i < 10; i++) {
      this.items.push({ id: i, title: 'book title ' + i });
    }
  }
}

The interface type for our data is shown below:

export interface Book {
  id: number;
  title: string;
}

When we press the refresh action to re-populate the data source with the latest data, the corresponding elements within the for-loop iterator will be removed and added back into the DOM of the HTML of the component. For all elements within loop this can become quite an expensive rebuild of the DOM for each data refresh.

The output from the for loop is show below:

To see the DOM manipulation in action, open the browser development developer tools by inspecting one of the list items.

You will see the list item elements:

<li>..</li>

After actioning Reset Book Data we can see all of the elements within the list re-generated as shown with a purple indicator (for Chrome browsers):

The above is the default behavior of iterators rendering within Angular templates.

NgFor with Tracked Iterations

I will now discuss how rendering of DOM elements works for elements within an iterator.

Unlike non-tracked iterators, with tracked iterators, the DOM elements are only regenerated when the values within the elements change. When values do not change, the corresponding DOM elements do not change.

To see how rendering of DOM elements occurs with tracking we will add a for loop that has tracking within the HTML template as shown:

<p>List of Books 1</p>

<li *ngFor="let item of items; index as i">
  <b>Book Entry:</b> {{ item.id }}, {{ item.title }}
</li>

<p>List of Books 2</p>

<li *ngFor="let item of items; index as i; trackBy: trackByBookFn">
  <b>Book Entry:</b> {{ item.id }}, {{ item.title }}
</li>

We will also simulate discrete changes to data by adding an additional action for updating book data as shown:

<p>Update even numbered Books</p>

<button (click)="updateBookData()">Update Book Data</button>

<p>Reset Book Data</p>

<button (click)="resetBookData()">Reset Book Data</button>

The corresponding typescript component code is shown below:

import { Component, VERSION } from '@angular/core';
import { Book } from './book';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  name = 'Angular ' + VERSION.major;
  items: Book[] = [];

  constructor() {
    this.resetBookData();
  }

  trackByBookFn(index, book) {
    return book.id;
  }

  resetBookData() {
    this.items = [];
    for (var i = 0; i < 10; i++) {
      this.items.push({ id: i, title: 'book title ' + i });
    }
  }

  updateBookData() {
    this.items
      .filter((b) => b.id % 2 == 0)
      .forEach((x) => {
        x.title = 'book title changed ' + x.id;
      });
  }
}

In the next section, I will explain why the tracking function helps the DOM to track objects that change.

How the Tracking Function works

To be able to track objects within the DOM, the tracking function requires an index and the object. It must return the unique id of the object. Angular uses the unique id to track which objects have changed and updates them accordingly without having to rebuild the DOM.

The tracking function that is used by the second iteration loop is shown below:

trackByBookFn(index, book) {
  return book.id;
}

It is referenced within the HTML template with the trackBy input parameter of the NgFor keyword:

<li *ngFor="let item of items; index as i; trackBy: trackByBookFn">

When we run the Reset Book Data action in the lists below:

When we view the HTML DOM elements within the inspector, we notice that the DOM elements for the first iteration loop have all regenerated, whereas the elements for the tracked DOM elements do not regenerate as their values are unchanged:

Delving into individual DOM elements, we can see that when the data update is performed on even numbered elements, when tracked, only DOM elements with changed data are regenerated:

When the data is refreshed, when tracked, only DOM elements that were changed data previously are regenerated back to their original values:

As we can see from the above example, and by observing changes to DOM elements within the browser development element inspector, we can observe when tracked and un-tracked changes of iteration elements update within the DOM of the component rendered HTML.

As for recommendations of when to use tracked loops within an application, for components where the size of loops is small (less than 100) and requires few refreshes, the rebuilding of the DOM is not as noticeable in terms of performance. When the size of the loops is into the thousands, the continual rebuilding of DOM is noticeable and can impact application performance and memory. With the larger loops, or where the size of loops can be large, it is recommended that we use a tracking function.

In another case, where we use input controls that bind to data that is from an iteration element object, and the element is part of a large loop, the use of tracking can improve performance when data is updated manually and frequently.

For details on the definition of the tracking function, you can refer to documentation on the Angular site.

That is all for today’s post.

I hope you have found this post both useful and informative.

Social media & sharing icons powered by UltimatelySocial