InterviewsVector

Auto-Migrate Angular Templates to @if / @for: The control-flow Schematic

Quick answer

Run ng generate @angular/core:control-flow and Angular rewrites your templates from *ngIf/*ngFor/*ngSwitch to the built-in @if/@for/@switch syntax automatically, adding the now-mandatory track expression for loops. It's incremental and the old structural directives keep working, so you can run it per-directory, review the diff, and commit in small steps. Complex or dynamic templates may need a manual touch-up afterward.

Short answer: Run ng generate @angular/core:control-flow and Angular rewrites your templates from *ngIf/*ngFor/*ngSwitch to @if/@for/@switch automatically, adding the now-mandatory track. It's incremental and non-breaking, so run it per-directory, review the diff, and commit in small steps.

Angular ships an official schematic that does the control-flow migration for you, so you don't hand-edit hundreds of templates.

The command

ng generate @angular/core:control-flow
# scope it to part of the app:
ng generate @angular/core:control-flow --path src/app/dashboard

Before → after

The schematic transforms the structural directives into blocks, adding track to every loop:

<!-- before -->
<div *ngIf="isLoggedIn">Welcome back!</div>
 
<!-- after -->
@if (isLoggedIn) {
  <div>Welcome back!</div>
}
<!-- before -->
<div *ngFor="let user of users; let i = index">{{ i }}: {{ user.name }}</div>
 
<!-- after -->
@for (user of users; track user; let i = $index) {
  <div>{{ i }}: {{ user.name }}</div>
}

Note the migrated @for has no let before the item and includes a track — that's the required new syntax (see the control-flow guide).

Run it safely

  1. Update to Angular 17+ first (ng update @angular/core @angular/cli).
  2. Scope with --path to migrate one area at a time.
  3. Review the diff — the migration is mechanical; nested or dynamic templates deserve a read.
  4. Fix track — the schematic often defaults to track $index; change it to a stable id (track user.id) where items can reorder, so Angular reuses DOM nodes.
  5. Commit in small chunks — the old and new syntax coexist, so partial migration is safe.

Common interview traps

  • Expecting it to be all-or-nothing — old directives keep working; migrate incrementally.
  • Shipping track $index everywhere — fine for static lists, but reorders lose node reuse.
  • Not reviewing complex templates — the schematic can produce awkward output for heavy nesting.

Key takeaways

  • The command is: ng generate @angular/core:control-flow (short: ng g @angular/core:control-flow).
  • It rewrites *ngIf/*ngFor/*ngSwitch to @if/@for/@switch and adds the required track expression.
  • It's incremental and non-breaking — the old directives still work, so migrate in small, reviewable chunks.
  • Run it per-directory (--path), review the diff, and commit; verify complex/nested templates by hand.

Frequently asked questions

How do I migrate an Angular app to the new control flow?

Run ng generate @angular/core:control-flow in the project. The schematic finds *ngIf, *ngFor, and *ngSwitch usages and rewrites them to @if, @for, and @switch, including a track expression for each loop. It's automated, but you should review the diff, especially for complex nested templates.

Do I have to migrate all at once?

No. The old structural directives and the new control flow can coexist, so the migration is safe to do incrementally. Scope the schematic to a directory with the --path option, review and commit, then repeat. Nothing breaks if some templates still use *ngIf.

What might the automatic migration get wrong?

It handles the common cases well but can leave awkward output for heavily nested or dynamically built templates, and it defaults loops to track $index. Review the diff and switch track to a stable identity (track item.id) where the list can reorder.

By Mohammad Wasi

Software Engineering Leader & Technical Author · Updated August 26, 2026


Related Posts