skip to content

When migrating Angular templates from *ngIf and NgSwitch to @if and @switch, which behaviour differences can change what renders?

level: seniorimportance: should knowfreq 40%

answer

  1. one branch versus every match
  2. equality changed in v17
  3. the host element moves inside
  4. leftover imports and templates

basics

~20 s

NgSwitch renders every matching case and every default, while @switch renders one branch, and pre-v17 NgSwitch matched with ==. *ngIf's host element moves inside the @if block, and a shared else template cannot simply be inlined.

solid answer

~40 s

Most migrations are mechanical, but a few differences change output. NgSwitch treats each `*ngSwitchCase` as an independent directive, so two cases with the same value both render, and several `*ngSwitchDefault` elements all appear; `@switch` renders only the first match or one `@default`. NgSwitch compared with `==` before v17, so very old templates may rely on `'1'` matching `1`, which `@switch`'s `===` does not. With `*ngIf`, the element carrying the directive is the conditional content; in `@if` you move that element inside the block, keeping its attributes. An else template referenced by `#ref` from several places, or passed into other components, cannot simply be inlined into one `@else`. Finally, remove the leftover `NgIf`, `NgSwitch` or `CommonModule` imports once nothing uses them.

code

html · 19 lines
html
<!-- before: both elements render for 'shipped' -->
<div [ngSwitch]="status">
  <p *ngSwitchCase="'shipped'">On its way.</p>
  <app-tracking *ngSwitchCase="'shipped'" />
  <p *ngSwitchDefault>Preparing your order.</p>
</div>

<!-- after: one case body holds both -->
<div>
  @switch (status) {
    @case ('shipped') {
      <p>On its way.</p>
      <app-tracking />
    }
    @default {
      <p>Preparing your order.</p>
    }
  }
</div>

go deeper

for a junior

Recall how *ngIf and NgSwitch map onto @if and @switch, including moving the host element inside the block.

for a middle

Explain the one-branch rule of @switch against NgSwitch's per-case matching and multiple defaults.

for a senior

Review a migration for duplicated cases, shared else templates and leftover imports, and back it with status-by-status tests.

for a principal

Plan the migration across a large codebase: automate the bulk, then target the known divergence patterns by search.

## Why migrate at all `NgIf`, `NgSwitch`, `NgSwitchCase` and `NgSwitchDefault` are **deprecated since v20** with intent to remove. The built-in blocks need no imports, type-check better and read in order. Angular ships an automated migration for this (the mechanics of running it belong to upgrade tooling); what matters in an interview is knowing where the old and new behaviour differ, because those are the lines a reviewer must check by hand. ## Differences that change what renders | Old behaviour | New behaviour | Risk | |---|---|---| | Every `*ngSwitchCase` that matches renders | Only the first matching `@case` renders | duplicated case values used to show two elements | | All `*ngSwitchDefault` elements render when nothing matches | At most one `@default` block | several default fragments must be merged | | NgSwitch matched with `==` before v17 | `@switch` matches with `===` | `'1'` vs `1` and `null` vs `undefined` cases | | `*ngIf` with `then` / `else` templates, possibly shared | `@if` / `@else` blocks are inline | shared templates must stay as `<ng-template>` | ### NgSwitch rendered all matches Each `*ngSwitchCase` is its own structural directive that asks the parent `NgSwitch` whether its value matches. They do not coordinate, so: ```html <div [ngSwitch]="status"> <p *ngSwitchCase="'shipped'">On its way.</p> <app-tracking *ngSwitchCase="'shipped'" /> </div> ``` shows **both** elements for `'shipped'`. That was even a documented way to show several elements for one case. A naive translation into two `@case ('shipped')` blocks renders only the first one. Merge them into one case body. ### Equality `@switch` uses strict equality. NgSwitch has also used `===` since v17, so a template that already worked on v17+ has no difference here, but code upgraded from before v17 may still carry cases that only matched through coercion. The v17 upgrade added a warning for that situation; if it was ignored, the migration to `@switch` does not reintroduce loose matching. ### Host elements and containers With `*ngIf`, the element that carries the asterisk **is** the conditional content: ```html <section class="refund" *ngIf="canRefund">...</section> ``` becomes ```html @if (canRefund) { <section class="refund">...</section> } ``` `<ng-container *ngIf>` wrappers, which existed only to host the directive, can disappear. When the host element also carried other directives, keep them on the element inside the block. ### Else templates used elsewhere `*ngIf="loaded; else spinner"` often points to a `<ng-template #spinner>` that other parts of the template, or `ngTemplateOutlet`, also use. Inlining it into one `@else` duplicates it; keep the shared template and render it with an outlet from the `@else` block instead. ## Differences that do not change output - **Truthiness** of `@if` matches `*ngIf`: `0` and `''` are still falsy. - **The as alias** has the same meaning: `*ngIf="x$ | async as x"` becomes `@if (x$ | async; as x)`. - **View lifetime** is the same: switching branches destroys and recreates the content. ## Reviewing an automated diff An automated conversion produces a large, uniform diff, and uniform diffs are easy to approve without reading. A focused review looks for the shapes above instead of reading every line: - A `[ngSwitch]` container whose cases reused a value, or that had more than one `*ngSwitchDefault`: check the converted cases manually. - An `else` or `then` template reference that appears more than once in the file: check that it was not duplicated or dropped. - Wrapper elements that existed only to host a structural directive: check that styling or layout did not depend on them, such as a flex container expecting a particular child. - Conversions of `*ngIf` with a pipe and `as`: check that the semicolon before `as` is present and that the alias name is unchanged. Everything else is usually safe to accept after the tests pass. ## After the migration 1. Remove `NgIf`, `NgSwitch`, `NgSwitchCase` and `NgSwitchDefault` from standalone `imports`; with `strictTemplates`, `unusedStandaloneImports` (NG8113) can point out imports the template no longer uses. 2. Keep `CommonModule` only if the template still uses something else from it, such as a pipe. 3. Search for `ngSwitchCase` values that appear twice in one switch, and for multiple `ngSwitchDefault` elements, before trusting a mechanical conversion. 4. Run the component tests that cover each status, since a lost second element does not fail compilation.

  • In Angular, what happens if two *ngSwitchCase elements match the same value, and how must that be migrated?
    Both render, because each case directive checks the value independently. `@switch` only renders the first matching `@case`, so a direct translation silently drops the second element. Merge the two into one `@case` body.
  • When migrating *ngIf with a shared else template in Angular, why not inline the template into @else?
    If the same `<ng-template #ref>` is used by several conditions or passed to `ngTemplateOutlet`, inlining it copies the markup into each place. Keep the named template and render it from the `@else` block with an outlet, so it stays defined once.

saying these in an interview costs you the question

  • @switch renders every matching @case, like NgSwitch
  • @if treats 0 as truthy, unlike *ngIf
  • The migration never changes rendered output, so no review is needed
  • CommonModule must stay imported for @if and @switch
  • Multiple *ngSwitchDefault elements map to multiple @default blocks