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 $indexonly 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/@forin new templates. - Track by stable identity.
- Use
@emptyfor 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
- Angular: Control Flow Migration
- Angular: NgIf API
- Angular: NgFor API
- Angular: Versioning and Releases
- Angular Roadmap