What is the purpose of *ngFor directive?
Angular templates provide iterative rendering to display collections of items. You can render collections using either the Classic Built-in Structural Directive (*ngFor) or the modern Built-in Control Flow (@for) introduced in Angular 17+.
1. Classic Built-in Syntax: *ngFor Directive
The *ngFor structural directive iterates over an iterable (such as an Array) and instantiates a template block for each item.
<!-- Classic *ngFor syntax (requires CommonModule or NgFor import) -->
<ul>
<li *ngFor="let user of users; let i = index; trackBy: trackByUserId">
{{ i + 1 }}. {{ user.name }} ({{ user.role }})
</li>
<li *ngIf="users.length === 0">No users found.</li>
</ul>
In your TypeScript component:
trackByUserId(index: number, user: User): number {
return user.id;
}
2. Modern Built-in Control Flow: @for Block (Angular 17+)
Angular 17 introduced declarative built-in control flow directly into the template compiler. The @for block requires zero imports (CommonModule is unnecessary) and provides a mandatory track expression for optimal DOM reconciliation:
<!-- Modern Built-in Control Flow syntax (Angular 17+) -->
<ul>
@for (user of users; track user.id; let i = $index) {
<li>{{ i + 1 }}. {{ user.name }} ({{ user.role }})</li>
} @empty {
<li>No users found.</li>
}
</ul>
3. Side-by-Side Comparison: Classic vs Built-in Control Flow
| Feature | Classic *ngFor Directive | Modern @for Built-in Block |
|---|---|---|
| Imports Required | Requires CommonModule or NgFor | ⚡ Zero imports (built directly into template compiler) |
| Tracking / Keys | Optional trackBy: fn (leads to DOM thrashing if omitted) | 🛡️ Mandatory track expression (track item.id or track $index) |
| Empty State | Requires separate *ngIf="users.length === 0" | Built-in @empty { ... } block |
| Contextual Variables | let i = index; let f = first | Built-in $index, $first, $last, $even, $odd, $count |
| Runtime Performance | Directive wrapper and comment anchor overhead | Up to 90% faster execution in benchmarks |