Control Flow and @defer

This section documents modern, standalone Angular — signals, the built-in @if / @for / @switch control flow, @defer, typed reactive forms, provideHttpClient, functional guards and interceptors, and server-side rendering with hydration — as described by the official documentation at angular.dev, which is the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against angular.dev before being relied on in production. Angular ships a major release roughly every six months and its APIs continue to evolve: the examples here target the current major release; where a consulted source disagrees with the current documentation, the documentation wins and the difference is noted.

This section’s bibliography lists the reference material consulted while preparing these pages.

Angular templates have keyword control-flow blocks — @if, @for, @switch — built into the compiler, plus @defer for loading part of a template lazily. None of them needs an import.

@if, @for, @switch

@if (user(); as u) {
  <p>Welcome, {{ u.name }}</p>
} @else if (loading()) {
  <app-spinner />
} @else {
  <a routerLink="/login">Sign in</a>
}

<ul>
  @for (item of items(); track item.id) {
    <li [class.first]="$first" [class.last]="$last">{{ $index + 1 }}. {{ item.label }}</li>
  } @empty {
    <li>No items ({{ $count }} total)</li>
  }
</ul>

@switch (status()) {
  @case ('active')  { <span class="ok">Active</span> }
  @case ('paused')  { <span class="warn">Paused</span> }
  @default          { <span>Unknown</span> }
}

Inside @for, the contextual variables are $index, $first, $last, $even, $odd, and $count. The track expression is required: it tells Angular how to identify a row across renders so it can move DOM nodes instead of destroying and recreating them. Use a stable unique key (item.id); use track $index only for lists of primitives that never reorder. A bad track causes lost focus, restarted animations, and wasted DOM work. See control flow.

Migrating from *ngIf / *ngFor / *ngSwitch

The old structural directives still work, but the schematic rewrites them automatically:

ng generate @angular/core:control-flow

Before — the structural directives:

<p *ngIf="user as u; else guest">
  Hi {{ u.name }}
</p>
<ng-template #guest><a>Sign in</a></ng-template>

<li *ngFor="let t of todos; trackBy: byId">
  {{ t.text }}
</li>

<div [ngSwitch]="status">
  <p *ngSwitchCase="'active'">Active</p>
  <p *ngSwitchDefault>Unknown</p>
</div>

After — the built-in control flow:

@if (user(); as u) {
  <p>Hi {{ u.name }}</p>
} @else {
  <a>Sign in</a>
}

@for (t of todos(); track t.id) {
  <li>{{ t.text }}</li>
}

@switch (status()) {
  @case ('active') { <p>Active</p> }
  @default { <p>Unknown</p> }
}

The built-in blocks need no CommonModule import, give better type-narrowing, and make track mandatory rather than optional. See control flow and the control-flow migration.

@defer blocks

A @defer block does not render (and its dependencies are not bundled into the initial JavaScript) until a trigger fires. Companion blocks cover the other states:

  • @placeholder — shown before the trigger; can declare a minimum display time.

  • @loading — shown while the deferred chunk downloads; supports minimum and after.

  • @error — shown if loading fails.

@defer (on viewport; prefetch on idle) {
  <app-comments [postId]="postId()" />
} @placeholder (minimum 500ms) {
  <p>Comments</p>
} @loading (after 100ms; minimum 1s) {
  <app-spinner />
} @error {
  <p>Could not load comments.</p>
}

Triggers: on idle (default), on viewport, on interaction, on hover, on timer(2s), on immediate, and when <expression>. on viewport, interaction, and hover may name a template reference variable to watch a different element. prefetch on <trigger> downloads the chunk early without rendering it yet. See deferrable views.

@defer state machine

stateDiagram-v2 [*] --> Placeholder Placeholder --> Loading: trigger fires Loading --> Complete: chunk loaded, block rendered Loading --> Error: load failed