Angular 18 Features — Signals, Control Flow and Deferrable Views

📋 Table of Contents
  1. Angular 18 Signals
  2. Common Mistakes

Angular 18 introduces several features that change how Angular applications are written — Signals replace some RxJS patterns for local state, the new built-in control flow syntax replaces structural directives, and deferrable views enable granular lazy loading. These features are opt-in and designed to work alongside existing Angular patterns. Part 5 uses them throughout the BlogApp, so understanding the basics here prevents confusion when they appear in the following chapters.

Angular 18 Signals

import { Component, signal, computed, effect, inject } from '@angular/core';
import { PostsService } from './posts.service';

@Component({
  selector:   'app-post-list',
  standalone:  true,
  template: `
    <!-- New built-in control flow syntax (Angular 17+) ──────────────────────
    Replaces *ngIf, *ngFor, *ngSwitch ────────────────────────────────────── -->

    @if (isLoading()) {
      <app-loading-spinner />
    } @else if (error()) {
      <app-error-message [message]="error()!" />
    } @else {
      @for (post of posts(); track post.id) {
        <app-post-card [post]="post" />
      } @empty {
        <p>No posts found.</p>
      }
    }

    <p>Showing {{ posts().length }} of {{ total() }} posts</p>

    <!-- Deferrable view — loads PostComments lazily when visible ─────────── -->
    @defer (on viewport) {
      <app-post-comments [postId]="selectedPostId()" />
    } @loading {
      <p>Loading comments...</p>
    } @error {
      <p>Failed to load comments.</p>
    } @placeholder {
      <div class="comments-placeholder">Comments will appear here</div>
    }
  `,
})
export class PostListComponent {
  private postsService = inject(PostsService);  // inject() instead of constructor

  // ── Signals — reactive state ───────────────────────────────────────────
  posts         = signal<PostSummaryDto[]>([]);
  isLoading     = signal(true);
  error         = signal<string | null>(null);
  total         = signal(0);
  selectedPostId = signal<number | null>(null);

  // ── Computed — derives from other signals ─────────────────────────────
  hasMore = computed(() => this.posts().length < this.total());

  // ── Effect — side effects when signals change ─────────────────────────
  constructor() {
    effect(() => {
      // Runs whenever selectedPostId changes
      console.log('Selected post changed to:', this.selectedPostId());
    });

    this.loadPosts();
  }

  private loadPosts() {
    this.postsService.getPublished(1, 10).subscribe({
      next:  result => {
        this.posts.set(result.items);
        this.total.set(result.total);
        this.isLoading.set(false);
      },
      error: err => {
        this.error.set('Failed to load posts.');
        this.isLoading.set(false);
      },
    });
  }
}
Note: Signals are Angular’s new reactive primitive. A signal (signal(value)) holds a value and notifies Angular when it changes — Angular re-renders only the components that read the signal. Computed signals (computed(() => ...)) automatically update when their dependencies change. Effects (effect(() => ...)) run side effects when signals change. Signals are simpler than RxJS for local component state: no Observable, no subscribe, no async pipe needed for synchronous values. Use Signals for component state; use RxJS for async HTTP calls and complex event streams.
Tip: The inject() function (used instead of constructor injection) is the modern Angular pattern for standalone components. It reads the DI container at the point of call, during component construction. private postsService = inject(PostsService) is equivalent to constructor(private postsService: PostsService) {}` but is more concise and works in composition functions (not just classes). Use inject() for service injection in components; constructor injection still works and is equally valid.
Warning: The new @if, @for, and @switch built-in control flow syntax is available in Angular 17+ and is the recommended approach for new Angular 18 code. The older *ngIf, *ngFor, and *ngSwitch structural directives still work but are being soft-deprecated. When you encounter tutorials or existing code using *ngFor, it is not wrong — it is just the older syntax. The BlogApp in Part 5 uses the new syntax throughout. Both syntaxes can coexist in the same project during migration.

Common Mistakes

Mistake 1 — Reading a signal value without calling it as a function

❌ Wrong — {{ posts }} in template displays the Signal object, not its value.

✅ Correct — {{ posts() }} calls the signal to read its current value; signals are getter functions.

Mistake 2 — Using effect() for state mutations (creates infinite loops)

❌ Wrong — effect(() => { this.posts.set(filter(this.posts())); }) — mutates a signal inside the effect that reads it; infinite loop.

✅ Correct — use computed() for derived values; use effect() for side effects only (logging, storage writes).

🧠 Test Yourself

A component has count = signal(0) and doubled = computed(() => this.count() * 2). When count.set(5) is called, what is the value of doubled()?