Under The Hood
frontend angular Angular 17+

Angular @for vs *ngFor

Last updated
Prerequisites:
Angular component basics
Understanding of *ngIf / *ngFor structural directives
  • angular
  • control-flow
  • templates
  • change-detection
  • diffing
  • track-by

Read at your depth

The practical view

In Angular 16 and earlier, repeating elements used the structural directive *ngFor="let item of items; trackBy: trackFn". Angular 17 introduced the new control-flow block syntax: @for (item of items; track item.id) { <p>{{ item.name }}</p> } @empty { <p>No items.</p> }. The big practical change is that track is now mandatory — if you omit it, the compiler errors out. The @empty block is new syntax that renders a fallback when the list is empty, replacing an *ngIf="items.length" wrapper. Items are scoped to the loop body with an implicit $index, $first, $last, $even, $odd, $count variables that are still available.

The same idea in other frameworks

react equivalent
items.map((item) => <li key={item.id}>{item.name}</li>)

Legacy vs modern

Legacy *ngFor with optional trackBy vs modern @for with mandatory track

The legacy structural directive forgot keys unless you remembered trackBy; the new control flow demands a track expression at compile time.

before → after
Legacy *ngFor
<ul>
  <li *ngFor="let task of tasks">
    {{ task.title }}
  </li>
</ul>
Modern @for
<ul>
  @for (task of tasks; track task.id) {
    <li>{{ task.title }}</li>
  } @empty {
    <li>No tasks yet.</li>
  }
</ul>

Interview gotchas

Context

Interviewers use this to separate developers who merely read the Angular 17 release notes from those who understand why the team made it a hard error.

The mechanical answer

Optional keys meant the happy path silently degraded to object-identity diffing. On every mutation that produced a new array — even an immutable update that only touched one element — Angular would tear down and re-create every row, losing checkbox state, scroll position inside rows, and causing full re-renders of expensive subtrees. The team decided the failure mode was too expensive to leave discoverable, so the compiler now rejects a loop without an explicit track expression. It is a static-analysis-level enforcement of the best practice that previously required code-review discipline.

Trap

Candidates often say "track is required so Angular knows how to update the DOM", which is true but shallow. The sharper point is that the DOM nodes are reused and component instances preserved, and a wrong key — like using the array index — makes the DOM correct but defeats state preservation, which is the entire purpose of the feature.

Under The Hood — a multi-depth technical interview hub.

Third-party licenses Press ⌘ K to search.