---
title: "Control Flow Is Not Just Cleaner Syntax"
description: "A production review lens for Angular control flow: when to migrate touched templates, how to choose `track`, and where `@switch` helps state modeling."
deck: "`@if`, `@for`, and `@switch` are most useful when they make UI states and list identity explicit. [Built-in control flow](https://angular.dev/guide/templates/control-flow) became stable in Angular v18 (May 2024) after the [RFC #50719](https://github.com/angular/angular/discussions/50719) closed. In production, the win is not syntax, it is what the new blocks force the team to name."
author: "André Ramos"
url: "https://andreramos.dev/angular/control-flow-not-just-cleaner-syntax/"
lang: "en"
type: "article"
---

# 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](https://angular.dev/guide/templates/control-flow) became stable in Angular v18 (May 2024) after the [RFC #50719](https://github.com/angular/angular/discussions/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.

*Bad dynamic list identity*

```html
@for (order of filteredOrders(); track $index) {
  <app-order-row [order]="order" />
}
```

*Template state with explicit identity*

```html
@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 `@for` with stable identity for dynamic data.
- Add `@empty` when the old UI silently rendered nothing.
- Use `@switch` for finite UI states instead of stacked conditions.
- Use `@defer` only 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

## Related

- [Standalone Without Turning NgModules Into a Villain](https://andreramos.dev/angular/standalone-without-ngmodule-dogma/)
- [A 30-Day Modernization Plan for Mature Angular Apps](https://andreramos.dev/angular/angular-30-day-modernization-plan/)
- [How I Would Adopt Signal Forms Now That They Are Stable in Angular 22](https://andreramos.dev/angular/signal-forms-not-my-default-yet/)
