Reader Stacks

Angular @if and @for Patterns and ngIf/ngFor Pitfalls

Modern Angular uses built-in @if and @for; legacy *ngIf and *ngFor remain important for maintenance but are deprecated from Angular 20.

For new Angular templates, use the built-in @if and @for blocks. Built-in control flow arrived in Angular 17 and became stable in Angular 18; current Angular documentation marks legacy NgIf and NgFor deprecated since Angular 20 in favor of @if and @for. Keep *ngIf/*ngFor examples for maintaining older code, but do not present them as the default path for a new Angular 22 application.

Modern conditional rendering with @if

@if (user) {
  <p>Welcome, {{ user.name }}</p>
} @else {
  <a routerLink="/login">Log in</a>
}

The inactive branch is not merely hidden with CSS; Angular controls whether that branch's view exists.

Modern list rendering with @for

<ul>
  @for (product of products; track product.id) {
    <li>{{ product.name }}</li>
  } @empty {
    <li>No products found.</li>
  }
</ul>

The track expression gives Angular stable identity across list updates. A persistent database/API ID is usually the best choice.

Tracking pitfalls

  • Do not generate random/new keys during rendering.
  • Use track $index only when item identity truly follows position and the list is effectively static.
  • For lists that insert, delete, sort, or reorder, prefer a stable item identifier.

Context variables and scope

@for (
  product of products;
  track product.id;
  let i = $index, first = $first, last = $last
) {
  <p>
    {{ i + 1 }}. {{ product.name }}
    @if (first) { <span>First</span> }
    @if (last) { <span>Last</span> }
  </p>
}

@for exposes contextual variables including $index, $count, $first, $last, $even, and $odd. Alias only what improves readability, particularly in nested loops.

Filtering: nested @if versus derived data

@for (product of products; track product.id) {
  @if (product.inStock) {
    <app-product-row [product]="product" />
  }
}

This is valid. If filtering is complex, repeated, or expensive, derive the collection in component code or a computed signal instead of turning the template into a data-processing layer.

Legacy *ngIf and *ngFor

<div *ngIf="user; else loggedOut">
  Welcome, {{ user.name }}
</div>

<ng-template #loggedOut>
  <a routerLink="/login">Log in</a>
</ng-template>

<li *ngFor="let product of products; trackBy: trackByProductId">
  {{ product.name }}
</li>

This syntax remains relevant for existing codebases, but Angular's current API marks NgIf and NgFor deprecated since v20.

Standalone components: legacy directives need imports

import { Component } from '@angular/core';
import { NgFor, NgIf } from '@angular/common';

@Component({
  selector: 'app-products',
  standalone: true,
  imports: [NgIf, NgFor],
  templateUrl: './products.component.html',
})
export class ProductsComponent {}

Built-in @if/@for belongs to template syntax and does not require these legacy directive imports.

Why two legacy * directives cannot share one element

<!-- Invalid legacy microsyntax -->
<li *ngFor="let product of products" *ngIf="product.inStock">
  {{ product.name }}
</li>

The * shorthand expands into an <ng-template>; two structural directives on one host cannot be unambiguously expanded. This restriction predates Angular 17—it was not introduced by built-in control flow.

Legacy code can wrap one directive in <ng-container>:

<ng-container *ngFor="let product of products; trackBy: trackByProductId">
  <li *ngIf="product.inStock">{{ product.name }}</li>
</ng-container>

Legacy trackBy

trackByProductId(index: number, product: Product): number {
  return product.id;
}

Legacy NgFor uses object identity by default. A stable trackBy result helps Angular reuse views when a new API response creates new object instances for logically unchanged items.

Safe migration to built-in control flow

ng generate @angular/core:control-flow

Angular provides this official schematic. Run it on a clean branch, review the template diff, and run unit/component/e2e tests. Pay special attention to named ng-template references, nested legacy directives, old trackBy behavior, and any lifecycle timing assumptions.

Version scope

This page targets Angular 22.x. Built-in control flow is available from Angular 17 and stable from Angular 18. Current API docs mark NgIf/NgFor deprecated from v20 with intent to remove them in a future major.

Migration checklist

  • Use @if/@for in new templates.
  • Track by stable identity.
  • Use @empty for empty states.
  • Move substantial filtering/business logic out of templates.
  • Import legacy directives only where older standalone templates still use them.
  • Use the official migration schematic and regression tests rather than blind search/replace.

Sources and further reading

Topics: Developer Productivity