Modern Angular
Control Flow Is Not Just Cleaner Syntax
@if, @for, and @switch are most useful when they make UI states and list identity explicit. Built-in control flow became stable in Angular v18 (May 2024) after the RFC #50719 closed. In production, the win is not syntax, it is what the new blocks force the team to name.
Cleaner is not enough
I like the new control flow because it reduces template gymnastics, but 'cleaner syntax' is not a strong enough reason to open a broad migration. In a mature app, I want the change to expose a real UI decision: loading, error, empty, ready, identity, or a finite state.
That framing keeps the migration useful. A touched template can become easier to review without turning the whole codebase into a syntax campaign.
What each block should prove
| Block | Good production use | Review question |
|---|---|---|
@if | Replace scattered ng-template branches | Did the main path and fallback become easier to read? |
@for | Render dynamic lists with explicit identity | Is track based on a stable id instead of convenience? |
@empty | Make empty states visible | Did the previous UI silently render nothing? |
@switch | Represent mutually exclusive UI states | Is the state model finite and typed? |
@defer | Delay genuinely heavy UI | Which JavaScript leaves the initial path? |
The list identity decision
@for forcing track is not busywork. It makes the team decide how the DOM should relate to data. That matters for rows with inputs, focus, animations, and repeated components with internal state.
If the backend gives you a stable id, use it. If it does not, ask whether the UI is hiding a modeling problem. Falling back to object identity or index can be acceptable for static lists, but it should not be the default for dynamic product data.
I would push back on track $index for a sortable or filterable product list. It can preserve the wrong row state when the order changes, which is exactly the kind of bug a syntax migration should not introduce.
@for (order of filteredOrders(); track $index) {
<app-order-row [order]="order" />
}@switch (ordersState()) {
@case ('loading') {
<app-orders-skeleton />
}
@case ('empty') {
<app-empty-orders />
}
@case ('ready') {
@for (order of orders(); track order.id) {
<app-order-row [order]="order" />
}
}
}A migration I would accept
I would accept a control flow PR when the team is already touching the screen and the diff improves one of three things: a clearer state model, a correct track decision, or a useful empty/fallback state.
I would push back on a repo-wide PR that only changes syntax. The code may compile, but the review cost is real and the product learned nothing.
Reusable artifact
Control flow review checklist
- Migrate touched templates, not the whole repo by default.
- Use
@forwith stable identity for dynamic data. - Add
@emptywhen the old UI silently rendered nothing. - Use
@switchfor finite UI states instead of stacked conditions. - Use
@deferonly when it changes loading cost or user experience.
Sources checked
- https://angular.dev/guide/templates/control-flow
- https://angular.dev/reference/migrations/control-flow
- https://angular.dev/guide/templates/defer
- https://github.com/angular/angular/discussions/50719
- https://blog.angular.dev/meet-angulars-new-control-flow-a02c6eee7843
- https://blog.angular.dev/angular-v18-is-now-available-e79d5ac0affe
Modern Angular Playbook
This article is one play.
The Modern Angular Playbook collects the diagnostic, the adoption matrix, eleven plays, and the 30-day plan. Free, in English and Portuguese.
You get both PDFs by email, through the Dojo IA list.
André Ramosavailable for remote roles, UTC−3Get in touch →