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/coreYour 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:
- Reduced Change Detection Cycles: Only components with actual changes update
- Optimized DOM Updates: Minimal DOM manipulation
- 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







Leave a Comment
Share Your Thoughts