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
items.map((item) => <li key={item.id}>{item.name}</li>)@for (item of items; track item.id) { <li>{{ item.name }}</li> } @empty { <li>Empty</li> }<li v-for="item in items" :key="item.id">{{ item.name }}</li>{#each items as item (item.id)} <li>{item.name}</li> {/each}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.
<ul>
<li *ngFor="let task of tasks">
{{ task.title }}
</li>
</ul><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.
Context
A classic 'do you understand the framework' question. The interviewer wants to hear about view containers and change-detection scope, not syntax.
The mechanical answer
*ngFor compiled to a wrapper directive that instantiated an EmbeddedView for every row via a TemplateRef. Each EmbeddedView had its own change-detection scope and the directive held an internal view-container bookkeeping object. @for is compiled directly into the component's generated view definition: the compiler emits repeaterCreate and related instructions operating on the LContainer of the parent view, so rows share the parent's change-detection scope and there is no directive wrapper class per loop. The runtime diff is a minimal edit script over track values applied with move/insert/remove operations on existing DOM nodes.
Trap
A common trap answer is that @for is "faster because it uses track by default". The diff algorithm existed for *ngFor too; the real structural change is the removal of the per-row embedded view scope and wrapper directive, which shrinks the change-detection graph and the generated code size. Mentioning the LContainer/TView mechanics is what makes the answer stand out.