DEV Community

Angular Old vs New Syntax: A Practical Cheat Sheet (*ngIf @if, Signals, and More)

Conditionals: *ngIf โ†’ @if

Old:

<div *ngIf="isLoggedIn; else guestBlock"> Welcome back! </div>
<ng-template #guestBlock>
  <div> Please log in. </div>
</ng-template>

New:

@if (isLoggedIn) {
  <div> Welcome back! </div>
} @else {
  <div> Please log in. </div>
}

No more <ng-template> gymnastics for the else branch. It's just a block, the way if/else looks everywhere else in the language.

Loops: *ngFor โ†’ @for

Old:

<li *ngFor="let user of users; trackBy: trackByUserId">
  {{ user.name }}
</li>
trackByUserId(index: number, user: User) {
  return user.id;
}

New:

@for (user of users; track user.id) {
  <li> {{ user.name }} </li>
} @empty {
  <li> No users found. </li>
}

Two real differences, not just shorter syntax: track is required, not optional - Angular won't let you skip it and silently re-render your whole list on every change like *ngFor would if you forgot trackBy. And @empty gives you a built-in empty state instead of a separate *ngIf="users.length === 0" block next to it.

Switch: *ngSwitch โ†’ @switch

Old:

<div [ngSwitch]="status">
  <p *ngSwitchCase="'active'"> Active </p>
  <p *ngSwitchCase="'inactive'"> Inactive </p>
  <p *ngSwitchDefault> Unknown </p>
</div>

New:

@switch (status) {
  @case ('active') {
    <p> Active </p>
  }
  @case ('inactive') {
    <p> Inactive </p>
  }
  @default {
    <p> Unknown </p>
  }
}

State: plain properties โ†’ signal()

This is the one that actually changes how change detection works, not just how it reads.

Old:

export class CounterComponent {
  count = 0;

  increment() {
    this.count++;
  }
}
<p> {{ count }} </p>

New:

export class CounterComponent {
  count = signal(0);

  increment() {
    this.count.update(c => c + 1);
  }
}
<p> {{ count() }} </p>

With a plain property, Angular's Zone.js-based change detection re-checks the whole component tree to figure out what changed. With a signal, Angular knows exactly which piece of the DOM depends on count and updates only that - no tree walk. The () on the template read isn't decoration, it's you telling Angular "track this specific value."

Derived values: getters โ†’ computed()

Old:

export class CartComponent {
  items: CartItem[] = [];

  get total() {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  }
}

New:

export class CartComponent {
  items = signal<CartItem[]>([]);
  total = computed(() => this.items().reduce((sum, item) => sum + item.price, 0));
}

A getter recalculates every time Angular checks the component, whether items changed or not. computed() only recalculates when a signal it actually reads changes, and caches the result otherwise.

Inputs: @Input() โ†’ input()

Old:

@Input() userName: string = '';

New:

userName = input<string>('');

Same idea as count above - userName() in the template, and anything derived from it via computed() only reruns when the input actually changes.

You don't have to migrate everything at once

Old and new syntax coexist fine in the same template and the same app - *ngIf in one component and @if in another won't break anything.

If you're staring down a large codebase, Angular ships an actual migration schematic for the control-flow syntax:

ng generate @angular/core:control-flow

It rewrites *ngIf / *ngFor / *ngSwitch to @if / @for / @switch across your templates automatically. Worth running on one module first and reviewing the diff before turning it loose on the whole app.

If you want this same side-by-side treatment for the rest of Angular - plus HTML, CSS, JS, Git, TypeScript, and Node - I put together a free course covering all of it: devinhyderabad.com. Still early, would genuinely value feedback on what's confusing or missing.

Comments

No comments yet. Start the discussion.