← Retour au blog
Angular 17control-flowperformancechange-detection@for @if @switch

Control Flow @if, @for, @switch in Angular 17+: Mastering Modern Declarative Syntax

Angular 17 marks a turning point: the retirement of the asterisk-based syntax (*ngIf, *ngFor, *ngSwitch) in favor of native declarative control flow. These redesigned structural directives—@if, @for, @switch—are far more than a cosmetic refresh. They deliver better traceability, finer change detection granularity, and a smaller error surface. For a production Angular developer, mastering these mechanisms is non-negotiable. The difference between a control flow that scales and one that lags with 10,000 items comes down to understanding how @for tracks elements and how @if isolates conditional branches.

The @if syntax: declaration without implicit magic

The @if directive replaces *ngIf with explicit, readable grammar. Instead of *ngIf="isVisible" , you write simply @if (isVisible) { } . The block body is standard template syntax—no ng-template wrapper required. The fundamental difference: @if evaluates the expression in the component's context without passing through an implicit context object. This means binding errors are caught earlier, at Angular's compile step (in strict mode). The major advantage emerges with the @else branch: instead of nesting two structures, you write @if (condition) { content } @else { alternative } . For multiple branches, chain them: @if (x > 10) { A } @else if (x > 5) { B } @else { C } . This reads like pseudocode, far clearer than a stack of nested *ngIf directives.

Concrete example: an e-commerce shopping cart form. Pre-Angular 17, you had three nested *ngIf directives to handle (empty cart, loading state, filled cart). With @if, the code reads like prose: @if (loading) { spinner } @else if (items.length === 0) { empty message } @else { items table } . Readability gains immediately, and future maintainers won't need to unfold three levels of template logic mentally.

The @for syntax: optimized iteration with mandatory trackBy

The @for directive replaces *ngFor with syntax closer to JavaScript: @for (item of items; track item.id) . Notice the track parameter—it's the key to optimization. In the old paradigm, you had to remember to implement a separate trackBy function and pass it as a parameter. Here, trackBy is mandatory and visible in the syntax. Angular refuses to compile your template if you omit how to track each element. This constraint is a gift in disguise: it enforces best practices and prevents catastrophic re-renders on long lists.

The track parameter accepts an expression or function. For a product list with unique IDs, you write track item.id . For nested structures, compose: track item.metadata.uuid . If you need more logic, delegate to a component method: track trackItem(item) where trackItem(item) { return item.id; } . Unlike *ngFor where trackBy was optional (and often forgotten), @for makes it syntactically impossible to skip. A list of 1,000 products with poor tracking = DOM thrashing and 500ms lag. The same list with @for and proper track = smooth 16ms per frame.

Practical example: rendering a paginated comment list. Each comment has a unique id . Before: *ngFor="let comment of comments; trackBy: trackByCommentId" plus a trackByCommentId method in the component. Now: @for (comment of comments; track comment.id) { <app-comment [data]="comment" /> } . The code is more compact, and any reviewer immediately sees tracking is in place.

The @switch syntax: alternatives without silent failures

@switch replaces ngSwitch + [ngSwitchCase] + [ngSwitchDefault]. The syntax becomes: @switch (value) { @case (1) { A } @case (2) { B } @default { C } } . No intermediate variable, no binding scattered across multiple directives. The Angular compiler validates that all possible cases are covered (if the type is an enum, for instance). This prevents silent bugs where a value falls through the cracks due to a missing case.

Use case: an order status component (pending, shipped, delivered, cancelled). Pre-Angular 17, you had an ngSwitch on a variable, then as many ngSwitchCase branches as statuses, plus ngSwitchDefault. Now: @switch (orderStatus) { @case ('pending') { pending icon and text } @case ('shipped') { tracking info } @case ('delivered') { review button } @default { error } } . Clarity gains enormously, and the TypeScript compiler reinforces type consistency.

Change detection and performance: the real win

These directives aren't merely syntactic rewrites. Angular 17 redesigned the change detection engine to work in tandem with these structures. Each @if, @for, @switch creates an isolated detection block. When a condition changes, only that block is re-evaluated, not the entire template. Combined with OnPush and signals, this isolation becomes even more potent: a granular signal state change triggers re-render only in blocks depending on that signal.

Take a page with 10 independent lists displayed conditionally. With *ngFor and *ngIf, any change in one list risks triggering a global check (unless you've already pushed OnPush everywhere, which isn't true in 80% of codebases). With @for and @if, each list is an island: modify one list, only its @for re-evaluates. On a dashboard with 200+ items, this translates to 50–70% reduction in change detection time.

The common pitfall: abusing track with complex expressions

Many developers, upon discovering @for, write: track item.name + item.category + item.timestamp . This is string concatenation that changes every render if any property mutates. Angular treats every item as new, even if they share the same logical ID. This destroys the optimization @for is meant to provide. Track must be an immutable, unique value per item: an ID, UUID, or at worst a stable composite key (e.g., ${item.userId}-${item.postId} ). Avoid expressions that depend on mutable or computed properties re-calculated each render.

Another pitfall: using the index as track. @for (item of items; track $index) works, but it's an anti-pattern. If you reorder the list, indices change, and Angular recreates all DOM nodes. You lose child input state and animations. The only exception: a static list where order never changes (rare).

Integration with signals and RxJS streams

If your component uses signals (Signal<Item[]>) or observables (Observable<Item[]>), @for works transparently. With signals, Angular automatically detects mutations and updates the DOM. With observables, pipe the async operator: @for (item of items$ | async; track item.id) . Caveat: the observable must be stable (not recreated every detection cycle), or you lose change detection benefits. Prefer signals when possible—they integrate natively with @for without boilerplate.

Migration and compatibility

Angular 17 still supports *ngIf, *ngFor, and ngSwitch. You aren't forced to migrate immediately. However, new codebases should start with @if, @for, @switch. For existing projects, migration is gradual: replace directive by directive, template by template. Automated migration tools (via ng update ) handle simple cases, but verify the result on complex patterns (trackBy with business logic, for example).

Conclusion

@if, @for, @switch aren't mere cosmetic aliases. They embody a philosophy of clarity and performance. TrackBy is no longer optional, conditions are explicit, and change detection is granular. If you're building a modern Angular app, these directives must become your instinct. They demand discipline (choosing track wisely), but the gains in maintainability and performance justify the effort. Test them on a small component, measure detection time with Angular DevTools, then migrate your codebase progressively.

Développeur Angular & Mobile freelance — Strasbourg.

© 2026 Emilien Pons — Tous droits réservés.Conçu avec Angular, PrimeNG et ❤️