Skip to main content

3 - New Template Syntax: @if, @for, @defer

Angular 17 introduced a new template control flow system with @if, @for, and @defer directives, replacing the historic ngIf and ngFor. These new syntaxes are more readable and enable better lazy loading. Learn the migration path.

What do you think about this article?

At a Glance

Angular 17 introduced a new template control flow system with @if, @for, and @defer directives, replacing the historic ngIf and ngFor. These new syntaxes are more readable and enable better lazy loading. Learn the migration path.

Angular's New Template Syntax: @if, @for, @defer

With Angular 17 the team introduced a new template control flow system that replaces the long-standing structural directives *ngIf, *ngFor, and ngSwitch. The new syntax using @if, @for, @switch, and @defer represents a fundamental shift in how Angular templates are written: more readable, more performant, and with entirely new capabilities like declarative lazy loading of components.

In this third article of the Modern Angular series, we will analyze each construct in depth: from basic syntax to advanced patterns, from automatic migration to community-proven best practices. If you are still using classic structural directives, this guide will show you why now is the time to migrate.

Modern Angular Series Overview

# Article Focus
1Angular SignalsFine-grained reactivity
2Zoneless Change DetectionRemoving Zone.js
3You are here - New Template @if, @for, @deferModern control flow
4Standalone ComponentsNgModule-free architecture
5Signal FormsReactive forms with Signals
6SSR and Incremental HydrationServer-Side Rendering
7Core Web Vitals in AngularPerformance and metrics
8Angular PWAProgressive Web App
9Advanced Dependency InjectionTree-shakable DI
10Migrating from Angular 17 to 21Migration guide

Why a New Syntax? The Limitations of Structural Directives

Structural directives *ngIf, *ngFor, and ngSwitch served Angular for nearly a decade, but they have structural limitations that the new syntax completely resolves.

Problems with Classic Structural Directives

  • Opaque microsyntax: Expressions like *ngFor="let item of items; trackBy: trackFn; let i = index; let first = first" are hard to read and remember
  • No native else-if: With *ngIf there is no else if: you must nest multiple *ngIf blocks or use ng-template with else
  • Bundle weight: Structural directives are Angular classes imported from CommonModule that must be included in the bundle
  • Implicit track: *ngFor does not require a trackBy function, causing silent performance issues
  • No lazy loading: There is no declarative mechanism to load components on-demand directly from the template
  • Verbosity: A simple if/else requires ng-template with template reference variables, increasing boilerplate

Let us look at a concrete example that shows the readability difference.

*ngIf with else: the old syntax

<!-- Old syntax: verbose and fragmented -->
<div *ngIf="user; else loadingTpl">
  <h2>{{ user.name }}</h2>
  <div *ngIf="user.role === 'admin'; else userTpl">
    <span class="badge admin">Administrator</span>
  </div>
  <ng-template #userTpl>
    <span class="badge user">User</span>
  </ng-template>
</div>
<ng-template #loadingTpl>
  <div class="spinner">Loading...</div>
</ng-template>
    
@if with @else: the new syntax

<!-- New syntax: clear and linear -->
@if (user) {
  <h2>{{ user.name }}</h2>
  @if (user.role === 'admin') {
    <span class="badge admin">Administrator</span>
  } @else {
    <span class="badge user">User</span>
  }
} @else {
  <div class="spinner">Loading...</div>
}
    

New Syntax Benefits at a Glance

Aspect Structural Directives New Control Flow
Required imports CommonModule or individual imports None (built into the compiler)
Else-if Not natively supported Native @else if
Mandatory track No (optional) Yes (mandatory in @for)
Lazy loading Not available @defer with declarative triggers
Bundle size Directives included in bundle Zero overhead (compiled)
Type checking Limited Complete
Readability Complex microsyntax Similar to JavaScript/TypeScript

@if and @else: Modern Conditional Rendering

The @if block replaces *ngIf with a syntax that closely resembles JavaScript control structures. It natively supports @else if and @else, eliminating the need for ng-template workarounds.

Basic Syntax

@if: basic syntax and @else

<!-- Simple condition -->
@if (isLoggedIn()) {
  <app-dashboard />
}

<!-- If with else -->
@if (user()) {
  <h2>Welcome, {{ user().name }}</h2>
} @else {
  <app-login-form />
}

<!-- If / else if / else -->
@if (status() === 'loading') {
  <div class="spinner">Loading...</div>
} @else if (status() === 'error') {
  <div class="error">An error occurred</div>
} @else if (status() === 'empty') {
  <div class="empty">No results found</div>
} @else {
  <app-data-table [data]="data()" />
}
    

Alias with the "as" Keyword

A powerful feature of @if is the ability to assign an alias to the result of the condition. This is particularly useful when working with Signals or Observables that return nullable values.

@if with alias (as)

<!-- Save the condition result in a local variable -->
@if (currentUser(); as user) {
  <div class="profile">
    <h3>{{ user.name }}</h3>
    <p>Email: {{ user.email }}</p>
    <p>Role: {{ user.role }}</p>
  </div>
}

<!-- Useful with async pipes or transformations -->
@if (users() | filterActive; as activeUsers) {
  <p>Active users: {{ activeUsers.length }}</p>
}
    

Key Differences from *ngIf

  • @if requires no imports (it is not a directive, it is part of the compiler)
  • @else if is natively supported, eliminating the need for nested ng-template blocks
  • The as syntax works as aliasing of the truthy value of the condition
  • Type narrowing works correctly: inside the @if block the type is already narrowed
  • No more CommonModule or direct import of NgIf is needed

@for with track: Performant and Mandatory Iteration

The @for block replaces *ngFor with a fundamental change: the track expression is mandatory. This design choice forces developers to define how Angular uniquely identifies each item in the list, preventing performance issues that went unnoticed with *ngFor.

Basic Syntax

@for: syntax with mandatory track

<!-- Basic iteration with track by id -->
@for (product of products(); track product.id) {
  <div class="product-card">
    <h3>{{ product.name }}</h3>
    <p>{{ product.price | currency:'EUR' }}</p>
  </div>
}

<!-- With implicit variables -->
@for (item of items(); track item.id; let i = $index; let isFirst = $first; let isLast = $last) {
  <div class="item" [class.first]="isFirst" [class.last]="isLast">
    <span class="index">{{ i + 1 }}.</span>
    <span>{{ item.name }}</span>
  </div>
}

<!-- Track by index (for lists without unique id) -->
@for (label of labels(); track $index) {
  <span class="tag">{{ label }}</span>
}
    

Implicit Variables Available in @for

Variable Type Description
$indexnumberCurrent index (0-based)
$firstbooleantrue if this is the first element
$lastbooleantrue if this is the last element
$evenbooleantrue if the index is even
$oddbooleantrue if the index is odd
$countnumberTotal number of elements

Why track is Mandatory

When Angular updates a list, it needs to decide which DOM elements to recreate, which to move, and which to keep unchanged. The track expression gives Angular a stable identity for each element, enabling optimal updates.

Performance comparison: proper track vs $index

// Component with a frequently updated list
@Component({
  selector: 'app-live-feed',
  standalone: true,
  template: `
    <!-- CORRECT: track by unique identity -->
    <!-- When the list changes, Angular reuses existing DOM nodes -->
    @for (post of posts(); track post.id) {
      <app-post-card [post]="post" />
    }

    <!-- DISCOURAGED: track by index -->
    <!-- If an element is added at the top, ALL nodes are recreated -->
    @for (post of posts(); track $index) {
      <app-post-card [post]="post" />
    }
  `
})
export class LiveFeedComponent {
  posts = signal<Post[]>([]);

  addNewPost(post: Post) {
    // With track post.id: only 1 DOM node created
    // With track $index: N DOM nodes recreated
    this.posts.update(list => [post, ...list]);
  }
}
    

When to Use $index as track

Use track $index only when the list items do not have a stable identity (for example, an array of simple strings). In all other cases, always prefer a unique property like id. Using $index on frequently changing lists is one of the main causes of poor performance in Angular applications.

@switch: Clean Multiple Selection

The @switch block replaces the combination of ngSwitch, *ngSwitchCase, and *ngSwitchDefault directives with a much more natural and readable syntax.

Comparison: ngSwitch vs @switch

<!-- BEFORE: ngSwitch (verbose, requires container) -->
<div [ngSwitch]="orderStatus">
  <div *ngSwitchCase="'pending'">
    <span class="badge warning">Pending</span>
  </div>
  <div *ngSwitchCase="'processing'">
    <span class="badge info">Processing</span>
  </div>
  <div *ngSwitchCase="'shipped'">
    <span class="badge success">Shipped</span>
  </div>
  <div *ngSwitchCase="'delivered'">
    <span class="badge success">Delivered</span>
  </div>
  <div *ngSwitchDefault>
    <span class="badge">Unknown</span>
  </div>
</div>

<!-- AFTER: @switch (clean, no container) -->
@switch (orderStatus()) {
  @case ('pending') {
    <span class="badge warning">Pending</span>
  }
  @case ('processing') {
    <span class="badge info">Processing</span>
  }
  @case ('shipped') {
    <span class="badge success">Shipped</span>
  }
  @case ('delivered') {
    <span class="badge success">Delivered</span>
  }
  @default {
    <span class="badge">Unknown</span>
  }
}
    

Notes on @switch

  • @switch uses strict comparison (===), identical to JavaScript
  • It does not require a container element: the comparison is directly on the expression
  • @default is optional but recommended to handle unexpected cases
  • It does not support fall-through: each @case is isolated (a desirable behavior)
  • Unlike ngSwitch, no imports from CommonModule are needed

@defer: Declarative Lazy Loading

@defer is the true revolution introduced with the new template syntax. It allows loading components, directives, and pipes on-demand directly from the template, without any TypeScript code for lazy loading. The Angular compiler automatically separates the code referenced inside @defer into separate chunks, downloaded only when the specified trigger activates.

Basic Syntax and Associated Blocks

@defer: complete structure with placeholder, loading, and error

@defer (on viewport) {
  <!-- Lazily loaded content -->
  <app-heavy-chart [data]="chartData()" />
} @placeholder {
  <!-- Shown before loading begins -->
  <div class="chart-placeholder">
    <p>The chart will appear here</p>
  </div>
} @loading (after 150ms; minimum 300ms) {
  <!-- Shown while the chunk is downloading -->
  <div class="spinner">Loading chart...</div>
} @error {
  <!-- Shown if loading fails -->
  <div class="error">Unable to load the chart</div>
}
    

Blocks Associated with @defer

Block Purpose Options
@placeholder Content shown before the trigger activates minimum: minimum display time
@loading Content shown while the chunk is downloading after: delay before showing; minimum: minimum duration
@error Content shown if loading fails No options

Available Triggers

@defer supports several triggers that determine when to begin loading the lazy content. They can be combined to create sophisticated loading conditions.

All @defer triggers

<!-- 1. on viewport: loads when the element enters the viewport -->
@defer (on viewport) {
  <app-comments [postId]="postId()" />
} @placeholder {
  <div style="height: 200px">Scroll to see comments</div>
}

<!-- 2. on idle: loads when the browser is idle -->
@defer (on idle) {
  <app-analytics-widget />
}

<!-- 3. on interaction: loads on click/focus on a specific element -->
<button #loadBtn>Show details</button>
@defer (on interaction(loadBtn)) {
  <app-product-details [id]="productId()" />
} @placeholder {
  <p>Click the button to see details</p>
}

<!-- 4. on hover: loads when the mouse hovers over an element -->
<div #previewArea>Hover for preview</div>
@defer (on hover(previewArea)) {
  <app-image-preview [url]="imageUrl()" />
}

<!-- 5. on timer: loads after a time interval -->
@defer (on timer(5s)) {
  <app-newsletter-popup />
}

<!-- 6. when: loads when a condition becomes true -->
@defer (when isAuthenticated()) {
  <app-admin-panel />
} @placeholder {
  <p>Log in to access the admin panel</p>
}

<!-- 7. Combining triggers -->
@defer (on viewport; on timer(10s)) {
  <app-sidebar-ads />
} @placeholder {
  <div class="ad-placeholder"></div>
}
    

@defer Trigger Summary

Trigger Activation Typical use case
on viewport The placeholder element enters the viewport Below-the-fold content, comments, footer
on idle The browser is idle (requestIdleCallback) Non-critical widgets, analytics
on interaction Click or focus on a specific element Expandable panels, on-demand details
on hover Mouse hover on a specific element Complex tooltips, previews
on timer(Ns) After N seconds/milliseconds Popups, delayed content
when condition When the expression becomes truthy State-based content (login, feature flags)

Intelligent Prefetching

@defer also supports prefetching: you can start downloading the chunk before the rendering trigger activates. This improves the user experience because the content is already ready when needed.

Prefetching with @defer

<!-- Prefetch on idle, render on viewport -->
<!-- The browser downloads the chunk when idle, -->
<!-- but renders it only when it enters the viewport -->
@defer (on viewport; prefetch on idle) {
  <app-heavy-data-grid [data]="gridData()" />
} @placeholder {
  <div class="grid-skeleton">
    <div class="skeleton-row"></div>
    <div class="skeleton-row"></div>
    <div class="skeleton-row"></div>
  </div>
}

<!-- Prefetch on hover, render on interaction -->
<!-- The user hovers and the chunk downloads, -->
<!-- when they click the content appears instantly -->
<button #editBtn>Edit Profile</button>
@defer (on interaction(editBtn); prefetch on hover(editBtn)) {
  <app-profile-editor [user]="currentUser()" />
} @placeholder {
  <p>Click to edit your profile</p>
}
    

@empty: Handling Empty Lists Gracefully

The @empty block is a companion to @for that allows showing alternative content when the iterated collection is empty. Before the new syntax, this required a separate *ngIf to check the array length.

@for with @empty: empty state handling

<!-- BEFORE: two separate directives -->
<div *ngIf="notifications.length === 0" class="empty-state">
  <p>No notifications</p>
</div>
<div *ngFor="let notification of notifications">
  {{ notification.message }}
</div>

<!-- AFTER: integrated into the control flow -->
@for (notification of notifications(); track notification.id) {
  <div class="notification" [class]="notification.type">
    <span class="icon">{{ notification.icon }}</span>
    <p>{{ notification.message }}</p>
    <time>{{ notification.date | date:'short' }}</time>
  </div>
} @empty {
  <div class="empty-state">
    <img src="/assets/no-notifications.svg" alt="No notifications" />
    <h3>All caught up!</h3>
    <p>You have no notifications at the moment.</p>
  </div>
}
    

Benefits of @empty

  • Eliminates the duplication of the array.length === 0 check
  • The @empty block is rendered only when the collection has zero elements
  • Improves readability by grouping iteration logic and empty state together
  • Works with any iterable: arrays, Signal arrays, filtered results

Automatic Migration with Schematics

The Angular team provides an official schematic that automatically migrates templates from the old syntax to the new one. This tool analyzes every template, converts the structural directives, and generates clean code.

Running the automatic migration

# Automatic control flow migration
ng generate @angular/core:control-flow-migration

# The command:
# 1. Analyzes all application templates
# 2. Converts *ngIf -> @if
# 3. Converts *ngFor -> @for (with track $index as default)
# 4. Converts ngSwitch -> @switch
# 5. Removes CommonModule imports where no longer needed

# For a single component
ng generate @angular/core:control-flow-migration --path=src/app/components/my-component

# For a dry run (preview without modifying files)
ng generate @angular/core:control-flow-migration --dry-run
    

Automatic Migration Caveats

  • Default track: The schematic inserts track $index as the default for all @for blocks. After migration, review each occurrence and replace with the correct unique property (e.g., track item.id)
  • Formatting: The migration may alter indentation. Run your project formatter after the migration
  • Complex templates: Templates with deeply nested structural directives may require manual review
  • Testing: Run all tests after migration to verify that behavior is unchanged
  • Commit first: Commit your code before running the migration so you can easily compare the differences
Example migration output

<!-- BEFORE migration -->
<div *ngIf="users$ | async as users; else loading">
  <table>
    <tr *ngFor="let user of users; trackBy: trackByUserId; let i = index">
      <td>{{ i + 1 }}</td>
      <td>{{ user.name }}</td>
      <td [ngSwitch]="user.status">
        <span *ngSwitchCase="'active'" class="green">Active</span>
        <span *ngSwitchCase="'inactive'" class="red">Inactive</span>
        <span *ngSwitchDefault class="gray">N/A</span>
      </td>
    </tr>
  </table>
</div>
<ng-template #loading>
  <div class="spinner">Loading...</div>
</ng-template>

<!-- AFTER automatic migration -->
@if (users$ | async; as users) {
  <table>
    @for (user of users; track user.id; let i = $index) {
      <tr>
        <td>{{ i + 1 }}</td>
        <td>{{ user.name }}</td>
        <td>
          @switch (user.status) {
            @case ('active') {
              <span class="green">Active</span>
            }
            @case ('inactive') {
              <span class="red">Inactive</span>
            }
            @default {
              <span class="gray">N/A</span>
            }
          }
        </td>
      </tr>
    }
  </table>
} @else {
  <div class="spinner">Loading...</div>
}
    

Advanced Patterns: Combining Control Flow

The true power of the new control flow emerges when you combine the various constructs to solve real-world scenarios. Let us explore some advanced patterns commonly used in production applications.

Pattern 1: Dashboard with Lazy Loading per Section

Dashboard with @defer for each widget

<div class="dashboard">
  <!-- Header always visible -->
  <header>
    <h1>Dashboard</h1>
    @if (user(); as u) {
      <span>Welcome, {{ u.name }}</span>
    }
  </header>

  <!-- KPIs loaded immediately when idle -->
  @defer (on idle) {
    <app-kpi-cards [metrics]="kpiData()" />
  } @placeholder {
    <div class="kpi-skeleton">
      @for (i of [1,2,3,4]; track $index) {
        <div class="skeleton-card"></div>
      }
    </div>
  }

  <!-- Chart loaded when it enters the viewport -->
  @defer (on viewport; prefetch on idle) {
    <app-revenue-chart [data]="revenueData()" />
  } @placeholder {
    <div class="chart-placeholder" style="height: 400px">
      <p>Revenue chart</p>
    </div>
  } @loading (after 200ms) {
    <div class="spinner">Loading chart...</div>
  }

  <!-- Data table loaded on demand -->
  <button #showTable>Show details</button>
  @defer (on interaction(showTable); prefetch on hover(showTable)) {
    @if (tableData(); as data) {
      @for (row of data; track row.id) {
        <app-data-row [row]="row" />
      } @empty {
        <p>No data available for the selected period</p>
      }
    }
  } @placeholder {
    <p>Click to view details</p>
  }
</div>
    

Pattern 2: List with Filters, Sorting, and Empty State

Complete list with nested control flow

<div class="product-list">
  <!-- Filters -->
  <div class="filters">
    @for (category of categories(); track category.id) {
      <button
        [class.active]="selectedCategory() === category.id"
        (click)="selectCategory(category.id)">
        {{ category.name }}
        <span class="count">({{ category.count }})</span>
      </button>
    }
  </div>

  <!-- Loading state -->
  @if (isLoading()) {
    <div class="loading">
      @for (i of [1,2,3]; track $index) {
        <div class="product-skeleton"></div>
      }
    </div>
  } @else {
    <!-- Product list -->
    @for (product of filteredProducts(); track product.id) {
      <div class="product-card">
        <h3>{{ product.name }}</h3>
        <p>{{ product.description }}</p>

        @switch (product.availability) {
          @case ('in_stock') {
            <span class="badge success">In Stock</span>
          }
          @case ('low_stock') {
            <span class="badge warning">Low Stock ({{ product.stock }})</span>
          }
          @case ('out_of_stock') {
            <span class="badge danger">Out of Stock</span>
          }
        }

        <div class="price">{{ product.price | currency:'EUR' }}</div>
      </div>
    } @empty {
      <div class="empty-state">
        <h3>No products found</h3>
        <p>Try adjusting your search filters</p>
        <button (click)="resetFilters()">Reset filters</button>
      </div>
    }
  }
</div>
    

Pattern 3: Conditional @defer with User Permissions

Lazy loading based on user role

@Component({
  selector: 'app-settings-page',
  standalone: true,
  template: `
    <h1>Settings</h1>

    <!-- Profile section: always available -->
    <app-profile-settings />

    <!-- Admin panel: loaded only if the user is admin -->
    @defer (when isAdmin()) {
      <app-admin-settings />
    } @placeholder {
      @if (!isAdmin()) {
        <div class="restricted">
          <p>Access restricted to administrators</p>
        </div>
      }
    }

    <!-- Advanced settings: defer + condition -->
    @if (showAdvanced()) {
      @defer (on idle) {
        <app-advanced-settings />
      } @loading {
        <div class="spinner">Loading advanced settings...</div>
      }
    }
  `
})
export class SettingsPageComponent {
  private authService = inject(AuthService);

  isAdmin = computed(() =>
    this.authService.currentUser()?.role === 'admin'
  );
  showAdvanced = signal(false);
}
    

Performance Impact

The new control flow is not just more readable: it has concrete, measurable advantages for application performance. The Angular compiler generates optimized code that eliminates the runtime overhead of structural directives.

Benchmarks: New Control Flow vs Structural Directives

Metric Directives (*ngIf, *ngFor) New Control Flow Improvement
Bundle size (CommonModule) ~12KB (minified) 0KB (built-in compiler) -100%
Rendering 1000-element list ~45ms ~38ms -15%
Re-rendering with single change ~8ms ~3ms -62%
Memory footprint per @for Directive + template ref + embedded view Compiled instructions only Significant
Lazy loading with @defer Not available (route-level only) Component/template-level New capability

Why the New Control Flow is Faster

  • Direct compilation: @if and @for blocks are compiled into optimized JavaScript instructions by the Angular compiler, without the overhead of creating directive instances at runtime
  • No ViewContainerRef: Structural directives use ViewContainerRef to create/destroy embedded views. The new control flow manages the DOM directly
  • Optimized track: The @for diffing algorithm has been rewritten from scratch to be more efficient than DefaultIterableDiffer
  • Tree shaking: Since they are not directives, they occupy no bundle space if unused. Even when used, the generated code is minimal
  • @defer chunk splitting: The compiler automatically creates separate chunks for lazy content, reducing the initial payload

Best Practices for the New Control Flow

After months of production usage by the Angular community, clear best practices have been established to get the most out of the new control flow.

10 Best Practices for the New Control Flow

  1. Always use a unique property in track: Avoid track $index unless the list contains primitive values without identity. Prefer track item.id
  2. Use @empty for empty states: Every @for that displays data to the user should have an @empty block with a helpful message
  3. Use @defer for below-the-fold content: Everything not visible at the first render should be wrapped in @defer (on viewport)
  4. Combine prefetch and trigger: Use prefetch on idle to pre-download chunks during browser idle moments
  5. Prefer @else if over @switch for few cases: If you have 2-3 conditions, @if / @else if is more readable than @switch
  6. Always provide a @placeholder for @defer: The user must understand that content is about to appear; an empty placeholder is confusing
  7. Use minimum and after in @loading: Avoid the loading "flash" for quickly-loading content with @loading (after 150ms; minimum 300ms)
  8. Do not nest too deeply: More than 3 levels of nested control flow makes the template unreadable. Extract sub-components instead
  9. Migrate completely: Do not mix old and new syntax in the same template; it confuses developers and complicates maintenance
  10. Review after automatic migration: The schematic uses track $index as default. Review every @for and assign the correct track expression

Anti-patterns to Avoid

Common mistakes with the new control flow

<!-- MISTAKE 1: Track by $index on dynamic lists -->
<!-- If an element is inserted at the top, ALL DOM nodes are recreated -->
@for (user of users(); track $index) {
  <app-user-card [user]="user" />  <!-- Unnecessarily recreated! -->
}
<!-- FIX: track user.id -->

<!-- MISTAKE 2: @defer without placeholder -->
<!-- The user sees empty space without understanding what is happening -->
@defer (on viewport) {
  <app-comments />
}
<!-- FIX: add @placeholder with visual indication -->

<!-- MISTAKE 3: @defer for critical above-the-fold content -->
<!-- Main content must not be lazy loaded -->
@defer (on idle) {
  <app-hero-section />  <!-- NEVER! The hero must be immediate -->
}

<!-- MISTAKE 4: Excessive nesting -->
@if (condition1()) {
  @for (item of items(); track item.id) {
    @if (item.visible) {
      @switch (item.type) {
        @case ('A') {
          @for (sub of item.children; track sub.id) {
            <!-- Too deeply nested! Extract a component -->
          }
        }
      }
    }
  }
}
<!-- FIX: extract sub-components to reduce nesting -->
    

Migration Checklist

Control Flow Migration Checklist

  1. Commit your current code before migration
  2. Run ng generate @angular/core:control-flow-migration --dry-run
  3. Review the proposed changes
  4. Run the actual migration
  5. Replace all track $index occurrences with unique properties
  6. Add @empty blocks where appropriate
  7. Evaluate where to add @defer for lazy loading
  8. Remove imports of CommonModule, NgIf, NgFor, NgSwitch
  9. Run the formatter to normalize indentation
  10. Run all tests (unit + e2e)
  11. Visually verify every page

Summary and Next Steps

Angular's new control flow represents a qualitative leap in the framework's developer experience. The syntax with @if, @for, @switch, and @defer is not just more readable: it produces more performant code, enables declarative lazy loading, and enforces best practices like mandatory track expressions.

Key Concepts from This Article

  • @if / @else if / @else: Replaces *ngIf with native support for else-if and aliasing via as
  • @for with track: Replaces *ngFor with mandatory track for guaranteed performance. Includes implicit variables ($index, $first, $last, $even, $odd, $count)
  • @switch / @case / @default: Replaces ngSwitch with clean syntax and strict comparison
  • @defer: Declarative lazy loading with triggers (viewport, idle, interaction, hover, timer, when) and prefetching
  • @empty: Graceful handling of empty lists, integrated directly into @for
  • Migration: The @angular/core:control-flow-migration schematic automates the conversion
  • Performance: Zero bundle overhead (compiled, not directives), optimized diffing, chunk splitting with @defer

In the next article of the series we will explore Standalone Components, the architecture that completely eliminates NgModules from Angular, simplifying the application structure and improving tree-shaking and lazy loading at the route level.

Frequently asked questions

What is the main reason for introducing a new template control flow system in Angular 17?

The new syntax replaces structural directives like ngIf, ngFor, and ngSwitch with more readable, performant, and feature-rich constructs.

How does the &#64;if directive differ from its predecessor ngIf in terms of readability?

The &#64;if directive natively supports if-else statements, eliminating the need for workarounds and making the code more similar to JavaScript control structures.

What is the purpose of the track expression in the &#64;for directive?

The track expression gives Angular a stable identity for each element in the list, enabling optimal updates when the list changes.

How does the &#64;defer directive improve application performance and user experience?

The &#64;defer directive allows loading components, directives, and pipes on-demand directly from the template, without any TypeScript code for lazy loading, which improves performance and user experience by prefetching content before rendering.

Sources and further reading

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects