What does Angular's control-flow migration change in your templates, and what does it leave for you to review?
answer
- structural directives become blocks
- trackBy becomes a track expression
- no trackBy means track by identity
- CommonModule only goes if unused
basics
~20 sAngular's control-flow migration rewrites *ngIf, *ngFor and *ngSwitch into @if, @for and @switch and drops the now-unused directive imports. It leaves review work: identity tracking where no trackBy existed, kept ng-templates, CommonModule still imported, and templates it could not parse.
solid answer
~40 s`ng generate @angular/core:control-flow` rewrites `*ngIf`, `*ngFor` and `*ngSwitch` into `@if`, `@for` and `@switch`, including `else`/`then` templates and `as` aliases, and removes the `NgIf`/`NgForOf`/`NgSwitch` imports. For `@for` it has to invent a `track` expression: a `trackBy: fn` becomes `track fn($index, item)`, and a loop **without** `trackBy` becomes `track item`, tracking by object identity. That is what to review first, because immutable updates then recreate every row; change it to a stable key such as `item.id`. It also keeps `ng-template`s that may be referenced elsewhere, removes `CommonModule` only when nothing else in the template (such as `ngClass` or the `async` pipe) needs it, and prints a warning listing templates it could not migrate, such as an aliased `*ngFor` collection.
code
html · 12 lines<!-- Before -->
<li *ngFor="let order of orders">{{ order.total }}</li>
<!-- After the migration -->
@for (order of orders; track order) {
<li>{{ order.total }}</li>
}
<!-- After your review: a stable key -->
@for (order of orders; track order.id) {
<li>{{ order.total }}</li>
}go deeper
Know which old directives the migration replaces with which blocks, and the command that runs it.
Explain how it builds the track expression from trackBy or identity, and why CommonModule and some ng-templates may remain.
Plan the review after the run: stable track keys, templates the migration refused, and components that relied on *ngFor remounting.
Decide whether to migrate a large codebase in one sweep or folder by folder, and how to keep the mechanical diff separate from clean-up.
The built-in control flow (`@if`, `@for`, `@switch`) arrived in Angular v17, and the `NgIf`, `NgForOf` and `NgSwitch` directives behind `*ngIf`, `*ngFor` and `*ngSwitch` have been deprecated since v20. The **control-flow migration** converts existing templates so you do not have to do it by hand. ## Running it ```shell ng generate @angular/core:control-flow ``` It is an **optional** schematic from `@angular/core` (alias `control-flow`), not something `ng update` applies for you. It has two options: | Option | Default | Meaning | | :-- | :-- | :-- | | `path` | `./` | Sub-path of the project to migrate, so you can go folder by folder | | `format` | `true` | Whether to reformat the migrated templates | ## What it rewrites - `*ngIf="cond"` becomes `@if (cond) { ... }`; `else` and `then` templates become `@else` branches, and `*ngIf="user$ | async as user"` becomes `@if (user$ | async; as user)`. - `*ngFor="let item of items"` becomes `@for (item of items; track ...) { ... }`, with `index` and other local aliases carried over. - `[ngSwitch]` with `*ngSwitchCase` / `*ngSwitchDefault` becomes `@switch` with `@case` / `@default`. - The `NgIf`, `NgForOf`, `NgSwitch` (and related) symbols are removed from the component's `imports` and from the TypeScript import declarations. ## What it has to invent: the `track` expression `@for` requires a `track` expression; `*ngFor` did not require a `trackBy`. The migration fills the gap mechanically: | Original | Migrated `track` | | :-- | :-- | | `*ngFor="let item of items; trackBy: trackById"` | `track trackById($index, item)` | | `*ngFor="let item of items"` | `track item` | `track item` preserves the old default (`*ngFor` without `trackBy` also compared items by identity), so row reuse does not change. But it is rarely what you want long-term: when a list is replaced with new objects, for example after re-fetching from a server, every row's DOM is destroyed and recreated. The first review task after the migration is to search for `track item`-style expressions and replace them with a stable key such as `track item.id`. The `trackBy` function calls it generated can usually be simplified to the key they returned. ## What it leaves behind 1. **`ng-template`s that might be used elsewhere.** The migration preserves existing `ng-template` elements in case another part of the template references them, so you may find unused templates to delete. 2. **`CommonModule`.** It is removed only if the template uses nothing else from it. If the template still uses `ngClass`, `ngStyle`, `ngTemplateOutlet` or a pipe such as `async` or `date`, `CommonModule` stays. The separate `common-to-standalone` schematic can later replace it with individual imports. 3. **Templates it refuses.** Some patterns stop it, for example a `*ngFor` with an aliased collection (`let x of items as list`) or an `*ngIf` with more than one alias. It then prints a `WARNING` listing each template and error, and that template needs manual work. 4. **Formatting.** With `format` on, templates are reformatted; review the diff for readability, especially inline templates. ## A behaviour difference worth knowing The docs list one breaking change: when a property used in the `track` expression changes but the object reference stays the same (in-place mutation), `@for` **updates the existing view's bindings**, including component inputs, instead of destroying and recreating it. `*ngFor` would remount the element if `trackBy` returned a different value. Components that relied on being recreated, for example to reset internal state, need checking. ## How to run it safely - Start from a clean branch and a green build. - Migrate with `path` one feature folder at a time on a large app. - Run the unit tests and visually check lists, empty states and `else` branches. - Commit the mechanical migration separately from the `track` clean-up, so reviewers can tell the two apart. Deciding which `@for` track expression is right and how `@if`/`@switch` blocks behave in detail belongs to the templates topics; this migration's job is to translate faithfully and leave you a short, known list of things to check.
- Why does the control-flow migration produce track item instead of track $index when there was no trackBy?Because *ngFor without trackBy compared items by identity, so track item preserves the old behaviour exactly. Tracking by index would change which DOM nodes get reused when items are inserted or reordered, which a faithful migration must not do.
- After the control-flow migration, why might CommonModule still be in a component's imports?The migration removes CommonModule only when the template uses nothing else from it. Directives such as ngClass or ngTemplateOutlet, or pipes such as async and date, keep it in place; the common-to-standalone schematic can then swap it for the individual imports.
saying these in an interview costs you the question
- The control-flow migration runs automatically during every ng update.
- Migrated @for loops always track by $index.
- The migration always removes CommonModule from migrated components.
- It deletes every ng-template it converts, so leftovers mean a bug.
- The migrated templates behave identically in every edge case.