Skip to the solution
MediumFrontend10 min read

How does the new control flow @if / @for / @switch replace *ngIf and *ngFor?

445 views

The problem

Question presented to candidate: "We have a few hundred templates full of *ngIf, *ngFor and ngSwitch. Walk me through how each one maps to the new @if, @for and @switch blocks, and how you would migrate the codebase safely."

What a strong answer should cover:

  • The mapping: *ngIf="x as y; else tpl" becomes @if (x; as y) { } @else { }; *ngFor="let t of list; trackBy: fn; let i = index" becomes @for (t of list; track t.id; let i = $index) { }; [ngSwitch] / *ngSwitchCase / *ngSwitchDefault become @switch / @case / @default.
  • Loop context variables gain a $ prefix ($index, $first, $last, $even, $odd, $count).
  • track is mandatory, and an existing trackBy function can be called from it.
  • The official schematic: ng generate @angular/core:control-flow, what it rewrites and what it leaves (it also removes the now-unused directive imports).
  • The old directives are deprecated since v20 but still work, so the migration can be gradual.
  • Verify after migrating: compile with strict templates and test lists, because @for uses a different diffing algorithm.

Clarifying questions expected:

  • "Is there a test suite I can rely on to catch behaviour changes after the schematic runs?"
  • "Do any templates use custom structural directives? Those are not touched by the migration."

Code / implementation expected: Yes β€” a before and after of the same component.

  • control-flow
  • angular-17
  • templates

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

10 min read Β· 9 sections

Read the code

AngularBefore: *ngIf with else, *ngFor with trackBy, and ngSwitch
import { Component, signal } from "@angular/core";
import { NgFor, NgIf, NgSwitch, NgSwitchCase, NgSwitchDefault } from "@angular/common";

type Ticket = { id: number; title: string; priority: "high" | "low" };

// BEFORE: structural directives (deprecated since v20, still working in v22).
@Component({
  selector: "app-root",
  imports: [NgIf, NgFor, NgSwitch, NgSwitchCase, NgSwitchDefault],
  template: `
    <p *ngIf="user() as u; else signedOut">Signed in as {{ u }}</p>
    <ng-template #signedOut><p>Please sign in</p></ng-template>

    <ul>
      <li *ngFor="let t of tickets(); trackBy: byId; let i = index; let odd = odd">
        {{ i }}{{ odd ? " (odd)" : "" }}: {{ t.title }}
        <ng-container [ngSwitch]="t.priority">
          <b *ngSwitchCase="'high'">[urgent]</b>
          <i *ngSwitchDefault>[later]</i>
        </ng-container>
      </li>
    </ul>
    <p *ngIf="tickets().length === 0">Inbox zero</p>

    <button (click)="user.set(null)">Sign out</button>
    <button (click)="tickets.set([])">Close all</button>
  `,
})
export class AppComponent {
  user = signal<string | null>("ada@example.com");
  tickets = signal<Ticket[]>([
    { id: 7, title: "Login broken", priority: "high" },
    { id: 9, title: "Typo in footer", priority: "low" },
  ]);
  byId = (_: number, t: Ticket) => t.id;
}
AngularAfter: the same component with @if, @for and @switch (identical output)
import { Component, signal } from "@angular/core";

type Ticket = { id: number; title: string; priority: "high" | "low" };

// AFTER: built-in control flow. Same output, no directive imports.
@Component({
  selector: "app-root",
  template: `
    @if (user(); as u) {
      <p>Signed in as {{ u }}</p>
    } @else {
      <p>Please sign in</p>
    }

    <ul>
      @for (t of tickets(); track t.id; let i = $index, odd = $odd) {
        <li>
          {{ i }}{{ odd ? " (odd)" : "" }}: {{ t.title }}
          @switch (t.priority) {
            @case ("high") { <b>[urgent]</b> }
            @default { <i>[later]</i> }
          }
        </li>
      }
    </ul>
    @if (tickets().length === 0) {
      <p>Inbox zero</p>
    }

    <button (click)="user.set(null)">Sign out</button>
    <button (click)="tickets.set([])">Close all</button>
  `,
})
export class AppComponent {
  user = signal<string | null>("ada@example.com");
  tickets = signal<Ticket[]>([
    { id: 7, title: "Login broken", priority: "high" },
    { id: 9, title: "Typo in footer", priority: "low" },
  ]);
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 59 of 121 in Angular

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Angular questions
Up nextMedium

How does two-way binding work under the hood?

Continue