Skip to content
Angular ng templates 4 min read

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.

ScenarioRecommended track
Objects with a stable unique idtrack item.id
Primitives (strings, numbers) that are uniquetrack item
No natural key, items never reordertrack $index

Prefer a stable, unique property such as a database id. Tracking by $index defeats 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.

VariableMeaning
$indexZero-based position of the current item
$countTotal number of items in the collection
$firsttrue for the first item
$lasttrue for the last item
$eventrue when $index is even
$oddtrue 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 track by a stable unique identifier (like an entity id) rather than $index when items can be added, removed, or reordered.
  • Reserve track $index for static lists or collections of primitives that never change position.
  • Use the @empty block instead of a separate @if check 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 with computed().
  • 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-flow migration schematic to convert legacy *ngFor usage automatically rather than rewriting by hand.
Last updated June 14, 2026
Was this helpful?