Angular Tutorial 0/42 lessons ~6 min read Lesson 22

    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.

    Course progress0%
    Focus
    53 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    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 track improves rendering efficiency and how contextual variables such as $index, $count, $first, $last, $even, and $odd work.
    • 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.

    text
    User Opens Products Page
    Loading?
    / \
    Yes No
    │ │
    ▼ ▼
    Spinner Error?
    / \
    Yes No
    │ │
    ▼ ▼
    Error Products?
    / \
    Yes No
    │ │
    ▼ ▼
    Product Empty
    List 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.

    text
    @if
    @else if
    @else
    @for
    @empty
    @switch
    @case
    @default

    A Simple Example

    typescript
    isLoggedIn = true;
    html
    @if (isLoggedIn) {
    <h2>Welcome back!</h2>
    }

    The @if Block

    The @if block conditionally renders template content.

    html
    @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

    typescript
    currentUser = signal<User | null>(null);
    html
    @if (currentUser()) {
    <app-dashboard />
    }

    @if and @else

    html
    @if (isLoggedIn) {
    <app-dashboard />
    } @else {
    <app-login />
    }

    This creates mutually exclusive UI branches.

    @else if

    html
    @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.

    html
    @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.

    html
    @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.

    typescript
    type PageState =
    | 'loading'
    | 'success'
    | 'empty'
    | 'error';
    state = signal<PageState>('loading');

    The @for Block

    The @for block renders collections.

    typescript
    products = [
    { id: 101, name: 'Laptop' },
    { id: 102, name: 'Phone' }
    ];
    html
    @for (product of products; track product.id) {
    <div>{{ product.name }}</div>
    }

    Understanding track

    The track expression tells Angular how to identify each item.

    html
    @for (product of products; track product.id) {
    <app-product-card [product]="product" />
    }
    text
    Initial List:
    101 Laptop
    102 Phone
    103 Tablet
    Updated List:
    101 Laptop
    102 Phone Pro
    103 Tablet
    101 -> Reuse
    102 -> Update
    103 -> 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

    html
    @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

    html
    @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.

    html
    @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

    html
    @for (item of items; track item.id) {
    @if ($first) {
    <span>First Item</span>
    }
    }

    $last

    html
    @if (!$last) {
    <hr />
    }

    $even and $odd

    html
    @for (row of rows; track row.id) {
    <div [class.alternate]="$odd">
    {{ row.name }}
    </div>
    }

    $count

    html
    @for (product of products; track product.id) {
    <p>Product {{ $index + 1 }} of {{ $count }}</p>
    }

    Aliasing Context Variables

    html
    @for (
    product of products;
    track product.id;
    let index = $index
    ) {
    <p>{{ index + 1 }}. {{ product.name }}</p>
    }

    The @empty Block

    html
    @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

    html
    @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.

    html
    @switch (status) {
    @case ('PENDING') {
    <span>Pending</span>
    }
    @case ('PROCESSING') {
    <span>Processing</span>
    }
    @case ('COMPLETED') {
    <span>Completed</span>
    }
    @default {
    <span>Unknown</span>
    }
    }

    @switch Architecture

    text
    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

    typescript
    role = signal<'ADMIN' | 'MANAGER' | 'USER'>('USER');
    html
    @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

    html
    @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

    html
    <!-- Legacy -->
    <div *ngIf="isLoggedIn">
    Welcome
    </div>
    <!-- Modern -->
    @if (isLoggedIn) {
    <div>Welcome</div>
    }
    html
    <!-- 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.

    text
    50,000 Records
    @for
    50,000 DOM Elements
    Browser Rendering Cost

    Large Data Architecture

    text
    Backend
    Paginated API
    Page of Data
    Angular
    @for + Stable Tracking
    Manageable DOM

    Real-World Dashboard Example

    html
    @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.

    typescript
    products = signal<Product[]>([]);
    loading = signal(true);
    error = signal<string | null>(null);
    html
    @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.

    typescript
    products = signal<Product[]>([]);
    searchTerm = signal('');
    visibleProducts = computed(() =>
    this.products().filter(product =>
    product.name
    .toLowerCase()
    .includes(this.searchTerm().toLowerCase())
    )
    );
    canManageProducts = computed(() =>
    this.currentUser()?.role === 'ADMIN'
    );
    html
    @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." />
    }
    text
    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.

    html
    @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.

    text
    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.

    text
    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.

    text
    Angular UI -> Hide Unauthorized Action -> Better UX
    Backend 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.

    html
    @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.

    20 questions
    1BeginnerQuestionWhat is built-in control flow in Angular?+

    Answer

    Built-in control flow is Angular's template syntax for conditional rendering and collection rendering, primarily using @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

    Tracking provides stable identity so Angular can associate data items with rendered views and reduce unnecessary DOM work.
    5IntermediateQuestionWhat should you use for track?+

    Answer

    For dynamic business entities, use stable unique identifiers such as 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

    Common variables include $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

    Yes, but excessive nesting should be avoided because it makes templates harder to maintain.
    10AdvancedQuestionHow would you handle loading, error, success, and empty states?+

    Answer

    Model the states clearly and render the correct UI branch. For complex workflows, prefer explicit state models over many independent booleans.
    11AdvancedQuestionHow would you render 100,000 records?+

    Answer

    Avoid rendering all records at once. Use server-side pagination, virtual scrolling, infinite scrolling, filtering, or lazy loading with stable tracking.
    12AdvancedQuestionIs track $index a good choice?+

    Answer

    It can be appropriate for static lists. For dynamic reorderable lists, stable business IDs are usually better.
    13AdvancedQuestionCan Angular control flow provide security?+

    Answer

    No. It can hide UI elements, but backend authorization must enforce real security.
    14AdvancedQuestionHow do Signals work with control flow?+

    Answer

    Signals can be read directly in control-flow expressions, and the UI updates when relevant Signal dependencies change.
    15AdvancedQuestionHow do you avoid complex Angular templates?+

    Answer

    Use derived state, computed() Signals, view models, smaller components, and clear state models.
    16AdvancedQuestionWhat is a common @for performance mistake?+

    Answer

    Using unstable tracking or assuming tracking makes enormous DOM collections cheap to render.
    17AdvancedQuestionHow would you migrate from *ngFor to @for?+

    Answer

    Understand existing 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

    Model page states explicitly, use stable tracking, virtualize or paginate large lists, extract nested sections, and keep business logic out of templates.
    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.

    text
    Application State
    Control Flow
    ┌─────┼─────────┐
    ▼ ▼ ▼
    @if @for @switch
    │ │ │
    ▼ ▼ ▼
    Show Repeat Select
    UI Items UI Case

    In a real enterprise application:

    text
    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.

    Ready to mark this lesson complete?Track your journey across the entire course.