Introduction to Angular 20 Signals

Angular 20, released in May 2025, marks a revolutionary milestone in reactive programming with the stabilization of the Signals API. The reactive programming model is now stable with production-ready APIs like effect(), linkedSignal(), and toSignal(). This comprehensive tutorial will guide you through mastering Angular's new reactive paradigm.

Signals represent a fundamental shift in how Angular applications manage state and reactivity. Signals are all about enabling very fine-grained updates to the DOM that are just not possible with the current change detection systems that we have available, ultimately leading to significant performance improvements by potentially eliminating the need for Zone.js.

Why Signals Matter in 2025

The introduction of stable signals in Angular 20 addresses several key challenges:

Performance Optimization: Improved Performance: Updates are limited to the elements that truly need to be changed

Developer Experience: Simplified state management with a more intuitive reactive model

Future-Proofing: Moving toward a zoneless architecture that aligns with modern web development practices

What's New in Angular 20

Angular 20 brings a wide range of updates: improvements to reactivity, SSR, support for zoneless architecture, template syntax enhancements, better tooling, and more. Key signal-related improvements include:

Stable Signal APIs

Angular 20 stabilizes several critical signal APIs:

  • signal() - Create writable signals
  • computed() - Derive values from other signals
  • effect() - Execute side effects when signals change
  • linkedSignal() - Create interconnected signal dependencies
  • toSignal() - Convert observables to signals

Traditional vs Reactive Approach

Traditional Angular (Zone.js-based):

// Traditional approach with manual change detection
export class TraditionalComponent {
  count = 0;
  doubleCount = 0;

  increment() {
    this.count++;
    this.doubleCount = this.count * 2; // Manual update
  }
}

Signal-based Reactive Approach:

// Reactive approach with automatic updates
export class ReactiveComponent {
  count = signal(0);
  doubleCount = computed(() => this.count() * 2); // Automatic update

  increment() {
    this.count.update(value => value + 1);
  }
}

Getting Started with Signals

Installation and Setup

To get started with Angular 20 signals, ensure you have the latest version:

npm install @angular/core@^20.0.0
ng update @angular/core

Your First Signal

Here's a simple example to get you started:

import { Component, signal, computed } from '@angular/core';

@Component({
  selector: 'app-counter',
  template: `
    <div>
      <p>Count: {{ count() }}</p>
      <p>Double: {{ doubleCount() }}</p>
      <button (click)="increment()">Increment</button>
      <button (click)="reset()">Reset</button>
    </div>
  `
})
export class CounterComponent {
  // Writable signal
  count = signal(0);
  
  // Computed signal (read-only)
  doubleCount = computed(() => this.count() * 2);

  increment() {
    this.count.update(value => value + 1);
  }

  reset() {
    this.count.set(0);
  }
}

Core Signal APIs

1. Creating Writable Signals

import { signal } from '@angular/core';

// Basic signal creation
const name = signal('Angular');
const age = signal(0);
const isActive = signal(true);

// Signal with complex data
const user = signal({
  id: 1,
  name: 'John Doe',
  email: 'john@example.com'
});

// Array signal
const items = signal(['item1', 'item2', 'item3']);

2. Reading and Writing Signal Values

// Reading signal values
console.log(name()); // 'Angular'
console.log(age()); // 0

// Setting new values
name.set('Angular 20');
age.set(25);

// Updating based on current value
age.update(current => current + 1);

// Mutating objects/arrays (use with caution)
user.mutate(u => u.name = 'Jane Doe');
items.mutate(arr => arr.push('item4'));

3. Computed Signals

You define computed signals using the computed function and specifying a derivation:

import { computed } from '@angular/core';

const firstName = signal('John');
const lastName = signal('Doe');

// Simple computed signal
const fullName = computed(() => `${firstName()} ${lastName()}`);

// Complex computed signal
const userSummary = computed(() => {
  const first = firstName();
  const last = lastName();
  const ageValue = age();
  
  return {
    displayName: `${first} ${last}`,
    canVote: ageValue >= 18,
    category: ageValue < 13 ? 'child' : ageValue < 20 ? 'teen' : 'adult'
  };
});

// Computed with conditional logic
const status = computed(() => {
  if (isActive()) {
    return age() >= 18 ? 'Active Adult' : 'Active Minor';
  }
  return 'Inactive';
});

4. Effects for Side Effects

Effects allow you to react to signal changes and perform side effects:

import { effect } from '@angular/core';

export class UserProfileComponent implements OnInit {
  userId = signal(1);
  userData = signal(null);
  
  ngOnInit() {
    // Effect runs when userId changes
    effect(() => {
      const id = this.userId();
      this.loadUserData(id);
    });
    
    // Effect for logging
    effect(() => {
      console.log('User data changed:', this.userData());
    });
  }
  
  private async loadUserData(id: number) {
    const data = await this.userService.getUser(id);
    this.userData.set(data);
  }
}

5. Converting Between Signals and Observables

import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { Observable } from 'rxjs';

export class DataService {
  private dataSubject = new BehaviorSubject(null);
  
  // Convert Observable to Signal
  data$ = this.dataSubject.asObservable();
  dataSignal = toSignal(this.data$, { initialValue: null });
  
  // Convert Signal to Observable
  countSignal = signal(0);
  count$ = toObservable(this.countSignal);
}

Advanced Signal Patterns

1. Linked Signals

Linked signals create bidirectional relationships between signals:

import { linkedSignal } from '@angular/core';

export class TemperatureConverter {
  celsius = signal(0);
  
  // Linked signal that converts and updates bidirectionally
  fahrenheit = linkedSignal(() => this.celsius() * 9/5 + 32);
  
  updateFahrenheit(f: number) {
    this.celsius.set((f - 32) * 5/9);
  }
}

2. Signal Composition Patterns

// Combining multiple signals
const loading = signal(false);
const error = signal(null);
const data = signal(null);

const state = computed(() => ({
  isLoading: loading(),
  hasError: !!error(),
  hasData: !!data(),
  isEmpty: !loading() && !error() && !data()
}));

// Conditional computed signals
const displayMessage = computed(() => {
  const currentState = state();
  
  if (currentState.isLoading) return 'Loading...';
  if (currentState.hasError) return `Error: ${error()}`;
  if (currentState.isEmpty) return 'No data available';
  return `Loaded ${data().length} items`;
});

Signals vs RxJS: When to Use What

When to Use Signals

✅ Use Signals for:

  • Simple state management
  • Synchronous computations
  • Template binding
  • Form state
  • UI-driven interactions

When to Use RxJS

✅ Use RxJS for:

  • Asynchronous operations
  • Complex event streams
  • HTTP requests
  • Time-based operations
  • Advanced operators (debounce, retry, etc.

Performance Benefits

Fine-Grained Reactivity

Updates are limited to the elements that truly need to be changed. This precision targeting results in:

  1. Reduced Change Detection Cycles: Only components with actual changes update
  2. Optimized DOM Updates: Minimal DOM manipulation
  3. Memory Efficiency: Reduced object creation and garbage collection

Conclusion

Angular 20 Signals represent a fundamental shift toward more efficient, intuitive reactive programming. The stable APIs provide a solid foundation for building modern web applications with:

  • Improved Performance: Fine-grained reactivity and reduced change detection overhead
  • Better Developer Experience: Simpler state management and clearer data flow
  • Future-Ready Architecture: Preparation for zoneless Angular and modern web