Control Flow: @for
Rendering a list is one of the most common things you do in a template, and Angular’s @for block is the modern, built-in way to do it. Introduced in Angular 17 as part of the new control flow syntax, @for replaces the older *ngFor structural directive with cleaner syntax, a mandatory track expression for fast updates, and a first-class @empty block for the “nothing to show” case. Because it is built into the framework, there is nothing to import — it just works inside any component template.
The basic @for block
A @for block iterates over any iterable (arrays, Set, Map, or anything implementing the iterable protocol) and renders its body once per item. The syntax declares a local variable, the collection to walk, and a required track expression.
import { Component, signal } from '@angular/core';
interface Product {
id: number;
name: string;
price: number;
}
@Component({
selector: 'app-catalog',
standalone: true,
template: `
<ul>
@for (product of products(); track product.id) {
<li>{{ product.name }} — \${{ product.price }}</li>
}
</ul>
`,
})
export class CatalogComponent {
products = signal<Product[]>([
{ id: 1, name: 'Keyboard', price: 79 },
{ id: 2, name: 'Mouse', price: 29 },
{ id: 3, name: 'Monitor', price: 249 },
]);
}
Output:
- Keyboard — $79
- Mouse — $29
- Monitor — $249
Note the parentheses after products — because the collection is a signal, you call it to read its current value. With a plain array property you would write track product.id over for (product of products; ...) instead.
Why track is required
The track expression tells Angular how to identify each item across change-detection cycles. When the collection changes, Angular uses the tracked key to decide which DOM nodes to keep, move, or destroy — rather than tearing down and rebuilding the whole list. This is the single most important performance lever for lists.
Unlike *ngFor, where trackBy was an optional optimization, track is mandatory in @for. The compiler will not let you omit it.
| Scenario | Recommended track |
|---|---|
| Objects with a stable unique id | track item.id |
| Primitives (strings, numbers) that are unique | track item |
| No natural key, items never reorder | track $index |
Prefer a stable, unique property such as a database id. Tracking by
$indexdefeats the purpose when items are inserted, removed, or reordered, because Angular will reuse the wrong DOM nodes and can produce subtle bugs with stateful elements like inputs.
Contextual variables
Inside the block, Angular exposes several read-only variables that describe the current iteration. You can rename them with let if you need to reference them in nested loops.
| Variable | Meaning |
|---|---|
$index | Zero-based position of the current item |
$count | Total number of items in the collection |
$first | true for the first item |
$last | true for the last item |
$even | true when $index is even |
$odd | true when $index is odd |
<ol>
@for (user of users(); track user.id; let i = $index, isLast = $last) {
<li [class.last-row]="isLast">{{ i + 1 }}. {{ user.name }}</li>
}
</ol>
The @empty block
A @for block can be immediately followed by an optional @empty block that renders when the collection has zero items. This removes the old pattern of pairing *ngFor with a separate *ngIf check.
<div class="results">
@for (result of searchResults(); track result.id) {
<article>{{ result.title }}</article>
} @empty {
<p class="muted">No results found. Try a different search.</p>
}
</div>
The @empty block must come directly after the closing brace of its @for block — nothing may sit between them.
Migrating from *ngFor
If you are upgrading an existing app, the Angular CLI ships an automated schematic that rewrites *ngFor, *ngIf, and *ngSwitch into the new control flow blocks.
ng generate @angular/core:control-flow
The conceptual mapping is direct:
<!-- Old -->
<li *ngFor="let item of items; trackBy: trackById">{{ item.name }}</li>
<!-- New -->
@for (item of items; track item.id) {
<li>{{ item.name }}</li>
}
Because @for is part of the template language rather than a directive, you no longer need to import NgFor or CommonModule for looping.
Best practices
- Always
trackby a stable unique identifier (like an entity id) rather than$indexwhen items can be added, removed, or reordered. - Reserve
track $indexfor static lists or collections of primitives that never change position. - Use the
@emptyblock instead of a separate@ifcheck to handle empty collections — it is more readable and avoids an extra DOM container. - Read signals with
()directly in the loop header; avoid recomputing derived arrays on every render by deriving them withcomputed(). - Rename contextual variables with
let(e.g.let i = $index) when nesting loops so the inner block can still reference the outer index. - Run the
control-flowmigration schematic to convert legacy*ngForusage automatically rather than rewriting by hand.