Skip to the solution
MediumFrontend8 min read

What is trackBy in ngFor and why use it?

564 views

The problem

Question presented to candidate: "Our product table uses *ngFor and polls the API every few seconds. Users complain the table flickers and loses their selection. What is trackBy, how would it help, and what is its equivalent in modern Angular?"

What a strong answer should cover:

  • By default *ngFor tracks items by object identity, so an API response full of new objects looks like a completely new list: every row is destroyed and recreated.
  • trackBy is a function (index, item) => key that tells the differ which items are "the same", so existing rows are kept and only their bindings update.
  • Benefits: fewer DOM operations, preserved focus/selection/input state, child components keep their state, CSS transitions do not restart.
  • The key must be stable and unique (an id), not the index for lists that reorder.
  • Gotcha: the function is called with this bound to the differ, not the component, so use an arrow function or avoid this.
  • Modern Angular: *ngFor is deprecated since v20; @for makes the same idea mandatory with track item.id.

Clarifying questions expected:

  • "Do the items have a unique id from the backend?"
  • "Is the app still on *ngFor, or can we move this table to @for?"

Code / implementation expected: Yes β€” a trackBy function on the component and its use in the template.

  • ngfor
  • trackby

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

8 min read Β· 10 sections

Read the code

Angular*ngFor with and without trackBy: counting the row elements Angular creates on refresh
import { Directive, Component, Input, signal } from "@angular/core";
import { NgFor } from "@angular/common";

type Product = { sku: string; name: string; price: number };

// Pretend API: every call returns brand-new objects, like JSON from a server.
const fetchProducts = (bump: number): Product[] => [
  { sku: "A1", name: "Desk", price: 200 + bump },
  { sku: "B2", name: "Chair", price: 90 + bump },
  { sku: "C3", name: "Lamp", price: 30 + bump },
];

// Counts how many row DOM elements Angular has created for each list.
export const created = { plain: signal(0), tracked: signal(0) };
@Directive({ selector: "[countRow]" })
export class CountRow {
  // queueMicrotask: never write state in the middle of a render pass.
  @Input() set countRow(list: "plain" | "tracked") {
    queueMicrotask(() => created[list].update((n) => n + 1));
  }
}

@Component({
  selector: "app-root",
  imports: [NgFor, CountRow],
  template: `
    <h4>*ngFor without trackBy: {{ created.plain() }} rows created</h4>
    <ul><li *ngFor="let p of products()" countRow="plain">{{ p.name }} {{ p.price }}</li></ul>

    <h4>*ngFor with trackBy: {{ created.tracked() }} rows created</h4>
    <ul><li *ngFor="let p of products(); trackBy: bySku" countRow="tracked">{{ p.name }} {{ p.price }}</li></ul>

    <button (click)="refresh()">Refresh prices</button>
  `,
})
export class AppComponent {
  created = created;
  private bump = 0;
  products = signal(fetchProducts(0));

  // Same items, new object references: only the price changed.
  refresh() {
    this.products.set(fetchProducts(++this.bump));
  }

  // trackBy: tell Angular what makes two items "the same item".
  // Angular calls it with `this` set to its differ, not the component: do not use `this` here.
  bySku(index: number, p: Product): string {
    return p.sku;
  }
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 46 of 121 in Angular

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Angular questions
Up nextMedium

How does the @defer block work and what triggers are available?

Continue