skip to content

Modernization Migrations

Core schematics rewrite old code: *ngIf to @if, NgModules to standalone, constructor injection to inject(), decorators to signal inputs, outputs and queries. Interviewers ask what each leaves behind.

part ofAngularoverview, primer and where to startread it →
on this pageshow

explore

questions

6

What does Angular's control-flow migration change in your templates, and what does it leave for you to review?

level: middleimportance: must knowfreq 55%

answer

  1. structural directives become blocks
  2. trackBy becomes a track expression
  3. no trackBy means track by identity
  4. CommonModule only goes if unused

basics

~20 s

Angular'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
html
<!-- 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

for a junior

Know which old directives the migration replaces with which blocks, and the command that runs it.

for a middle

Explain how it builds the track expression from trackBy or identity, and why CommonModule and some ng-templates may remain.

for a senior

Plan the review after the run: stable track keys, templates the migration refused, and components that relied on *ngFor remounting.

for a principal

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.
open as a page

You own a v15-era Angular codebase now updated to v22; in what order would you run the modernization schematics, feature by feature, and why?

level: seniorimportance: must knowfreq 46%

basics

~20 s

Run the optional schematics after the version update, one feature folder at a time: control flow, then the three standalone modes, then inject(), then the signal input, output and query migrations, and self-closing tags last, testing between each.

open as a page

How does Angular's standalone migration convert an NgModule-based app, and why must its three modes run in order?

level: middleimportance: should knowfreq 45%

basics

~20 s

Angular's standalone migration runs in three passes: convert components, directives and pipes to standalone, prune NgModules that became empty, then switch bootstrapModule to bootstrapApplication. Each pass relies on the previous one, and each needs a build check between runs.

open as a page

When you update to Angular v22, what does the Eager change-detection migration do to existing components, and why?

level: middleimportance: should knowfreq 40%

basics

~10 s

Because v22 made OnPush the default, the v22 update migration adds changeDetection: ChangeDetectionStrategy.Eager to every component that set no value and renames explicit Default to Eager, so existing components behave exactly as before.

open as a page

Why does Angular's signal-input migration skip some @Input fields, and how do you deal with the ones it leaves?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Angular's signal-input migration only converts inputs it can prove safe; fields written to, accessor inputs, inherited conflicts, spyOn targets, @HostBinding use or template narrowing are skipped. Use --insert-todos to see why, then fix by hand or opt into --best-effort-mode.

open as a page

What does Angular's self-closing-tags migration change in templates, and why does it leave an empty <div></div> alone?

level: juniorimportance: nice to knowfreq 18%

basics

~10 s

Angular's self-closing-tags migration rewrites empty component and custom-element tags such as <app-spinner></app-spinner> into <app-spinner />. It skips standard HTML elements like <div>, which cannot self-close in HTML, and any tag with real content.

open as a page