Angular Control Flow
Learn modern Angular Control Flow, including @if, @else if, @else, @for, track, @empty, @switch, @defer, Signals, computed(), loading/error/success/empty UI states, legacy directive migration, performance, enterprise architecture, anti-patterns, and interview questions.
Learning Objectives
By the end of this lesson, you will understand:
- What control flow means in Angular templates and how modern built-in control flow works.
- How to use
@if,@else if,@else,@for,@empty,@switch,@case, and@default. - How
trackimproves rendering efficiency and how contextual variables such as$index,$count,$first,$last,$even, and$oddwork. - How to build loading, success, empty, and error UI states.
- How to handle large collections, avoid common control-flow mistakes, and design enterprise-grade templates.
Introduction
Every real-world application needs to decide what appears on the screen: whether the user is logged in, whether data is loading, whether an API failed, whether products exist, which role a user has, and which rows should appear in a table.
User Opens Products Page│▼Loading?/ \Yes No│ │▼ ▼Spinner Error?/ \Yes No│ │▼ ▼Error Products?/ \Yes No│ │▼ ▼Product EmptyList State
Angular's built-in control flow lets you express these UI decisions directly in templates.
What Is Angular Control Flow?
Control flow determines whether content should be rendered, which content should be rendered, and how collections should be rendered.
@if@else if@else@for@empty@switch@case@default
A Simple Example
isLoggedIn = true;
@if (isLoggedIn) {<h2>Welcome back!</h2>}
The @if Block
The @if block conditionally renders template content.
@if (isLoggedIn) {<button>Logout</button>}
When the condition is true, Angular renders the block. When false, it does not render it.
Real-World Authentication Example
currentUser = signal<User | null>(null);
@if (currentUser()) {<app-dashboard />}
@if and @else
@if (isLoggedIn) {<app-dashboard />} @else {<app-login />}
This creates mutually exclusive UI branches.
@else if
@if (score >= 90) {<p>Excellent</p>} @else if (score >= 70) {<p>Good</p>} @else if (score >= 50) {<p>Average</p>} @else {<p>Needs Improvement</p>}
Using @if with an Alias
When an expression is used repeatedly inside a block, alias it with as.
@if (userProfile(); as user) {<h2>{{ user.name }}</h2><p>{{ user.email }}</p>}
Real-World API State
A typical API-driven page should represent loading, success, empty, and error states explicitly.
@if (isLoading()) {<app-loading-spinner />} @else if (error()) {<app-error-message [message]="error()!" />} @else if (products().length > 0) {<app-product-list [products]="products()" />} @else {<app-empty-state />}
Avoid Boolean Explosion
Independent booleans such as isLoading, hasError, hasData, and isEmpty can create impossible states. For complex workflows, model the state explicitly.
type PageState =| 'loading'| 'success'| 'empty'| 'error';state = signal<PageState>('loading');
The @for Block
The @for block renders collections.
products = [{ id: 101, name: 'Laptop' },{ id: 102, name: 'Phone' }];
@for (product of products; track product.id) {<div>{{ product.name }}</div>}
Understanding track
The track expression tells Angular how to identify each item.
@for (product of products; track product.id) {<app-product-card [product]="product" />}
Initial List:101 Laptop102 Phone103 TabletUpdated List:101 Laptop102 Phone Pro103 Tablet101 -> Reuse102 -> Update103 -> Reuse
Why Tracking Matters
Stable identity helps Angular associate existing rendered views with data items, reducing unnecessary DOM work. For dynamic collections, prefer stable unique identifiers such as product.id, user.id, order.id, or transaction.id.
Tracking by $index
@for (item of items; track $index) {{{ item }}}
Tracking by position can be reasonable for truly static collections. For inserted, deleted, or reordered lists, stable IDs are usually better.
Tracking by Identity
@for (item of items; track item) {...}
This relies on object identity. For API data or immutable transformations, object references can change even when the logical entity is the same.
Contextual Variables in @for
Angular provides contextual variables such as $count, $index, $first, $last, $even, and $odd.
@for (product of products; track product.id) {<div>{{ $index + 1 }}. {{ product.name }}</div>}
$index
$index is the current zero-based position. Use $index + 1 for display numbering.
$first
@for (item of items; track item.id) {@if ($first) {<span>First Item</span>}}
$last
@if (!$last) {<hr />}
$even and $odd
@for (row of rows; track row.id) {<div [class.alternate]="$odd">{{ row.name }}</div>}
$count
@for (product of products; track product.id) {<p>Product {{ $index + 1 }} of {{ $count }}</p>}
Aliasing Context Variables
@for (product of products;track product.id;let index = $index) {<p>{{ index + 1 }}. {{ product.name }}</p>}
The @empty Block
@for (product of products; track product.id) {<app-product-card [product]="product" />} @empty {<p>No products available.</p>}
@empty avoids writing a separate condition solely for empty collections.
Real-World Search Example
@for (course of searchResults(); track course.id) {<app-course-card [course]="course" />} @empty {<app-empty-state message="No courses found." />}
The @switch Block
When one value can produce several UI states, @switch can be cleaner than a long @if chain.
@switch (status) {@case ('PENDING') {<span>Pending</span>}@case ('PROCESSING') {<span>Processing</span>}@case ('COMPLETED') {<span>Completed</span>}@default {<span>Unknown</span>}}
@switch Architecture
Order Status│▼@switch├── PENDING├── PROCESSING├── COMPLETED└── @default
@switch vs @if
Use @if for different boolean conditions. Use @switch when comparing one expression against multiple known values, such as status, role, payment state, or order type.
Real-World Role-Based UI
role = signal<'ADMIN' | 'MANAGER' | 'USER'>('USER');
@switch (role()) {@case ('ADMIN') {<app-admin-dashboard />}@case ('MANAGER') {<app-manager-dashboard />}@case ('USER') {<app-user-dashboard />}@default {<app-access-denied />}}
UI hiding is not security. The backend must still enforce authorization.
Nested Control Flow
@if (isLoggedIn()) {@for (notification of notifications(); track notification.id) {@if (notification.unread) {<app-notification [notification]="notification" />}}}
Avoid Deep Template Nesting
Deeply nested control-flow blocks become difficult to maintain. Extract responsibilities into smaller components such as OrdersPage, OrderList, OrderCard, and OrderStatus.
Modern Control Flow vs Legacy Structural Directives
<!-- Legacy --><div *ngIf="isLoggedIn">Welcome</div><!-- Modern -->@if (isLoggedIn) {<div>Welcome</div>}
<!-- Legacy --><div *ngFor="let product of products; trackBy: trackProduct">{{ product.name }}</div><!-- Modern -->@for (product of products; track product.id) {<div>{{ product.name }}</div>}
Why Modern Control Flow Is Valuable
Modern control flow gives readable template logic, built-in empty states, explicit tracking, cleaner branches, and less structural directive syntax.
Migrating Existing Applications
When migrating from *ngIf, *ngFor, or *ngSwitch, preserve behavior, stable tracking semantics, conditional rendering, empty states, and performance characteristics. Do not treat migration as blind text replacement.
Performance with Large Lists
Good tracking does not make rendering 50,000 DOM elements free. For large datasets, consider pagination, server-side pagination, virtual scrolling, infinite scrolling, filtering, and lazy data loading.
50,000 Records│▼@for│▼50,000 DOM Elements│▼Browser Rendering Cost
Large Data Architecture
Backend│▼Paginated API│▼Page of Data│▼Angular│▼@for + Stable Tracking│▼Manageable DOM
Real-World Dashboard Example
@if (loading()) {<app-loader />} @else if (error()) {<app-error-state />} @else {@for (user of users(); track user.id) {<article><h3>{{ user.name }}</h3>@switch (user.status) {@case ('ACTIVE') {<span>Active</span>}@case ('SUSPENDED') {<span>Suspended</span>}@default {<span>Unknown</span>}}</article>} @empty {<app-empty-state />}}
Control Flow with Signals
Modern Angular control flow works naturally with Signals.
products = signal<Product[]>([]);loading = signal(true);error = signal<string | null>(null);
@if (loading()) {<app-loader />} @else if (error()) {<app-error-state />} @else {@for (product of products(); track product.id) {<app-product-card [product]="product" />} @empty {<app-empty-state />}}
Control Flow with computed()
Templates should read simple reactive state. Use computed() to derive permissions, filtered lists, and view-model flags from source Signals.
products = signal<Product[]>([]);searchTerm = signal('');visibleProducts = computed(() =>this.products().filter(product =>product.name.toLowerCase().includes(this.searchTerm().toLowerCase())));canManageProducts = computed(() =>this.currentUser()?.role === 'ADMIN');
@if (canManageProducts()) {<button>Add Product</button>}@for (product of visibleProducts(); track product.id) {<app-product-card [product]="product" />} @empty {<app-empty-state message="No matching products." />}
Source Signals│▼computed()│▼Template Control Flow│▼Declarative UI
The @defer Block
@defer lazily loads template content based on triggers such as viewport, idle, hover, timer, or interaction. It is ideal for below-the-fold widgets and expensive secondary UI.
@defer (on viewport) {<app-comments [postId]="post.id" />} @placeholder {<p>Scroll down to load comments...</p>} @loading (after 100ms; minimum 300ms) {<app-spinner />} @error {<p>Could not load comments.</p>}
A blog post that defers its comment section until the user scrolls can reduce initial bundle work and improve Lighthouse scores.
Initial Render│▼Primary Content│▼Trigger (viewport / idle / hover)│▼@defer loads secondary UI
Enterprise Control Flow Architecture
In large applications, control flow should sit on top of explicit state models, stable list identity, reusable UI components, and performance boundaries.
Backend API│▼Application State│├── loading()├── error()├── success data└── empty data│▼Derived View Models (computed)│▼@if / @for / @switch / @defer│├── Page Components├── Feature Components└── Shared Empty/Error/Loading UI
Enterprise teams usually keep business rules in services or computed Signals, use stable IDs in @for, paginate or virtualize large datasets, and defer non-critical UI until it is actually needed.
Control Flow and Security
Control flow can hide unauthorized UI elements, but it cannot secure backend operations. Always enforce permissions on the server.
Angular UI -> Hide Unauthorized Action -> Better UXBackend API -> Verify Authorization -> Actual Security
Control Flow and Accessibility
Dynamic content should consider assistive technologies. Errors can use role="alert", and loading messages can use aria-live where appropriate.
@if (error()) {<div role="alert">{{ error() }}</div>}@if (loading()) {<div aria-live="polite">Loading data...</div>}
Common Pitfalls
Using Unstable Tracking
Avoid tracking with values that change frequently. Prefer stable business IDs.
Tracking by Index for Reorderable Lists
If items can be inserted, deleted, or moved, $index does not represent logical identity.
Rendering Massive Collections
Tracking helps reconciliation, but huge DOM trees still cost the browser time.
Complex Business Logic in Templates
Derive complex permissions or view models outside the template.
Excessive Nested Control Flow
Extract reusable components when templates become deeply nested.
Using UI Conditions as Security
Hiding buttons is not authorization. The backend must enforce access control.
Calling Expensive Methods Repeatedly
Prefer Signals, computed(), view models, or memoized reactive state.
Enterprise Best Practices
- Model complex UI state explicitly with loading, success, empty, and error states.
- Use stable IDs for dynamic business data in
@for. - Keep templates declarative and avoid large business-logic expressions.
- Extract complex UI into smaller components.
- Design useful empty states with explanation, action, retry, or search guidance.
- Separate loading, empty, and error experiences.
- Optimize large lists with server-side pagination, stable tracking, virtualization, and lazy loading.
Common Anti-Pattern: Unstable track Expressions
Avoid tracking with values that change frequently such as random values or unstable object references.
Prefer stable business identifiers such as product.id, user.id, or order.id for dynamic entities.
Common Anti-Pattern: Using track $index for Dynamic Lists
Tracking by position does not represent logical item identity when lists can be inserted, deleted, or reordered.
Use stable unique IDs for reorderable business collections.
Common Anti-Pattern: Rendering Massive Collections
Good tracking does not make rendering 100,000 DOM elements inexpensive.
Combine pagination, virtualization, filtering, and lazy data loading with stable tracking.
Common Anti-Pattern: Complex Business Logic in Templates
Putting permission calculations or expensive methods directly in template conditions makes templates hard to test and maintain.
Derive view models with computed() Signals or services, then keep templates declarative.
Common Anti-Pattern: Using @if as Security
Hiding an admin button with @if does not prevent direct API access.
Enforce authorization on the backend while using control flow only for UX.
Advanced interview questions
Interview Prep
Practice concise answers, then expand each card for the explanation.
1BeginnerQuestionWhat is built-in control flow in Angular?+
Answer
@if, @for, and @switch with related blocks.2BeginnerQuestionWhat is the difference between @if and @switch?+
Answer
@if evaluates conditional expressions. @switch compares one expression against multiple specific cases.3BeginnerQuestionWhat is @for?+
Answer
@for renders a block for each item in an iterable collection and uses a track expression to identify items.4IntermediateQuestionWhy is track important in @for?+
Answer
5IntermediateQuestionWhat should you use for track?+
Answer
product.id, user.id, or order.id. For static collections, $index may be acceptable.6IntermediateQuestionWhat is @empty?+
Answer
@empty is an optional block attached to @for that renders when the collection has no items.7IntermediateQuestionWhat contextual variables are available in @for?+
Answer
$index, $count, $first, $last, $even, and $odd.8IntermediateQuestionWhat is the difference between @for and *ngFor?+
Answer
@for is the modern built-in syntax with direct track expressions and @empty. *ngFor is the older structural directive syntax.9IntermediateQuestionCan @if and @for be nested?+
Answer
10AdvancedQuestionHow would you handle loading, error, success, and empty states?+
Answer
11AdvancedQuestionHow would you render 100,000 records?+
Answer
12AdvancedQuestionIs track $index a good choice?+
Answer
13AdvancedQuestionCan Angular control flow provide security?+
Answer
14AdvancedQuestionHow do Signals work with control flow?+
Answer
15AdvancedQuestionHow do you avoid complex Angular templates?+
Answer
computed() Signals, view models, smaller components, and clear state models.16AdvancedQuestionWhat is a common @for performance mistake?+
Answer
17AdvancedQuestionHow would you migrate from *ngFor to @for?+
Answer
trackBy behavior, convert to @for with equivalent stable tracking, then test insert, delete, reorder, empty, and rendering behavior.18AdvancedQuestionHow would you design control flow for an enterprise dashboard?+
Answer
19AdvancedQuestionWhat is @defer and when should you use it?+
Answer
@defer lazily loads template content based on triggers such as viewport, idle, hover, or interaction. It is useful for below-the-fold widgets and expensive secondary UI.20AdvancedQuestionHow does computed() help control-flow templates?+
Answer
computed() derives memoized view state from Signals so templates can read simple reactive values instead of calling expensive methods or duplicating business logic.Summary
Angular control flow is the decision-making layer of your template.
Application State│▼Control Flow│┌─────┼─────────┐▼ ▼ ▼@if @for @switch│ │ │▼ ▼ ▼Show Repeat SelectUI Items UI Case
In a real enterprise application:
Backend API│▼Application State│┌──────────────┼──────────────┐▼ ▼ ▼Loading Error Success│ │ │▼ ▼ ▼@if @if @for│▼Stable Tracking│┌────────────┴────────────┐▼ ▼Data Empty│ │▼ ▼Components @empty│▼@switch│┌────────────┼────────────┐▼ ▼ ▼Active Pending Failed
Keep control flow declarative: let the component model the state and let the template clearly describe what the user should see. Combined with Signals, computed(), stable tracking, explicit state models, reusable components, pagination, virtualization, and @defer, modern control flow provides a clean foundation for scalable UI.
Next Lesson
Angular Signals — Next, you'll learn what Signals are, why Angular introduced them, signal(), set(), update(), computed(), effect(), dependency tracking, writable and read-only Signals, Signal inputs, Signals with HTTP and RxJS, Signals vs BehaviorSubject, change detection, shopping-cart state, enterprise architecture, performance optimization, anti-patterns, and interview questions.