Why is trackBy important in *ngFor, and what breaks without it?

LowIntermediateAngular
Preparing for interviews?

Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.

Quick Answer

trackBy tells Angular how to uniquely identify items in an *ngFor list so it can reuse existing DOM nodes and component instances when the array changes. Without it (or with a bad trackBy), Angular may destroy and recreate rows unnecessarily, which hurts performance and can reset UI state like input values, focus, and child component state. TrackBy preserves DOM state and performance; test with reorders and large lists.

Answer

Overview

*ngFor maintains a list of embedded views (DOM + directives + child components). trackBy defines each item's identity so Angular can do efficient diffs: move/update existing views instead of destroy/recreate them.

HTML
<ul>
  <li *ngFor="let user of users; trackBy: trackByUserId">
    <app-user-row [user]="user"></app-user-row>
  </li>
</ul>
                  
TYPESCRIPT
type User = { id: string; name: string };

export class UsersComponent {
  users: User[] = [
    { id: 'u1', name: 'Ada' },
    { id: 'u2', name: 'Linus' }
  ];

  trackByUserId(index: number, user: User): string {
    return user.id;
  }

  // Common real-world cause of problems: new object references on refresh
  refreshFromApiLike(): void {
    // Same ids, but NEW objects => without trackBy, Angular treats them as new items
    this.users = this.users.map(u => ({ ...u }));
  }
}
                  

What trackBy controls

What Angular can do

Why it matters

Item identity

Match old items to new items across changes

Prevents treating “same logical item” as a brand-new row

DOM reuse

Reuse existing DOM nodes and directives

Avoids heavy DOM churn and layout/repaint work

Component instance reuse

Keep child component instances for the same logical item

Preserves child state and avoids repeated ngOnInit/ngOnDestroy

Row movement

Move existing views when sorting/reordering

Keeps UI stable instead of flickering or re-initializing

trackBy lets Angular perform a stable, minimal update of the rendered list

Without trackBy (or with unstable identity)

What breaks / degrades

Typical symptom

Array recreated with new object references

Rows are destroyed + recreated even if ids are the same

Inputs reset, focus/caret jumps, child state resets

Expensive child rows (components, heavy templates)

More DOM work + more lifecycle work

Scroll jank, CPU spikes, visible flicker on updates

UI state stored in the DOM (focus, selection, <input> value while typing)

DOM state is lost on re-create

User is typing and the caret jumps / typed text disappears

3rd-party widgets inside rows (charts, editors)

Widgets get torn down and rebuilt

Re-initialization costs + lost internal widget state

Bad trackBy (random, Date.now, returning a new object)

Identity changes every check => Angular rebuilds everything

Worst-case: every CD cycle behaves like a full rerender

Most “trackBy bugs” show up when you refresh data or rebuild arrays (common with immutability patterns)

TrackBy choice

When it's correct

When it causes bugs

Stable unique id (recommended)

Items have a real unique key (id, uuid, slug)

Rarely; only if the id is not actually unique/stable

Composite key (e.g., ${type}:${id})

Multiple item types share the same id space

If any part of the composite can change over time

Value itself (primitives)

List is strings/numbers and values are unique/stable

If values can repeat (duplicates) => identity collisions

Index (avoid unless list is static)

List never reorders/inserts/removes, only updates values in-place

On insert/reorder, state sticks to the wrong row (wrong input value on wrong item)

Key rule: identity must represent the logical item, not its current position

Extra note

Modern Angular also has @for with a track expression (same concept): @for (user of users; track user.id) { ... }. The principle is identical: stable identity => stable DOM + preserved state.

Practical scenario
A table with editable rows should preserve input focus when new data arrives from the server.

Common pitfalls

      • Using index as trackBy, which breaks on reorders.
      • Returning non-unique ids, causing UI glitches.
      • Skipping trackBy and forcing full DOM re-creation.
Trade-off or test tip
TrackBy improves performance but requires stable ids. Test by reordering and verifying focus retention.

Summary

trackBy is important because it gives Angular a stable key per item so it can reuse and move existing rows instead of recreating them. Without it (especially when data refresh creates new object references), you pay in performance and you lose UI state (inputs, focus, child component state). Use a stable unique id; avoid index unless the list is truly static.

Similar questions
Guides
37 / 37