Angular Signals: The Future of Reactive Programming
Explore Angular Signals, a new reactive primitive that simplifies state management and improves performance with fine-grained reactivity.
- Angular
- Signals
- Reactive Programming
- TypeScript
- Angular 16+
Angular Signals, introduced in Angular 16, represent a fundamental shift in how we handle reactivity in Angular applications. After working with Signals in Angular 18 projects like Danone BAPEN and Ship-Watch, I've found them to be a game-changer for performance and developer experience. Signals provide fine-grained reactivity, automatic change detection optimization, and a simpler mental model than RxJS Observables for many use cases.
What are Signals?
Signals are reactive primitives that hold a value and notify consumers when that value changes. They're similar to Observables but simpler for many scenarios, with automatic dependency tracking and optimized change detection.
Basic Signal Usage
Create and use signals:
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<p>Count: {{ count() }}</p>
<p>Double: {{ doubleCount() }}</p>
<button (click)="increment()">Increment</button>
`,
})
export class CounterComponent {
// Writable signal
count = signal<number>(0);
// Computed signal (derived state)
doubleCount = computed(() => this.count() * 2);
increment(): void {
this.count.update(value => value + 1);
}
// Effect runs when dependencies change
constructor() {
effect(() => {
console.log('Count changed:', this.count());
});
}
}
Signal Operations
Creating Signals
import { signal, computed } from '@angular/core';
// Writable signal with initial value
const count = signal<number>(0);
// Signal with object
const user = signal<User>({ id: '1', name: 'John' });
// Read-only signal (computed)
const doubleCount = computed(() => count() * 2);
// Computed with multiple dependencies
const total = computed(() => count() + anotherCount());
Updating Signals
// Set value directly
count.set(10);
// Update using current value
count.update(value => value + 1);
// Update object signals
user.update(user => ({ ...user, name: 'Jane' }));
// Mutate object (if using mutable pattern)
user.mutate(user => {
user.name = 'Jane';
});
Signals in Components
Use signals for component state:
import { Component, signal, computed } from '@angular/core';
interface Todo {
id: number;
text: string;
done: boolean;
}
@Component({
selector: 'app-todo-list',
template: `
<input #input (keyup.enter)="addTodo(input.value)" />
<ul>
<li *ngFor="let todo of todos()">
<input
type="checkbox"
[checked]="todo.done"
(change)="toggleTodo(todo.id)"
/>
{{ todo.text }}
</li>
</ul>
<p>Completed: {{ completedCount() }}</p>
`,
})
export class TodoListComponent {
todos = signal<Todo[]>([]);
completedCount = computed(() =>
this.todos().filter(todo => todo.done).length
);
addTodo(text: string): void {
this.todos.update(todos => [
...todos,
{ id: Date.now(), text, done: false }
]);
}
toggleTodo(id: number): void {
this.todos.update(todos =>
todos.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
);
}
}
Signals with Input/Output
Use signals for component inputs and outputs:
import { Component, input, output, signal } from '@angular/core';
@Component({
selector: 'app-user-card',
template: `
<div>
<h3>{{ user().name }}</h3>
<p>{{ user().email }}</p>
<button (click)="onSelect.emit(user().id)">Select</button>
</div>
`,
})
export class UserCardComponent {
// Signal-based input
user = input.required<User>();
// Signal-based output
onSelect = output<string>();
// Computed from input signal
displayName = computed(() => this.user().name.toUpperCase());
}
Effects
Effects run side effects when signals change:
import { Component, signal, effect } from '@angular/core';
@Component({
selector: 'app-search',
template: `
<input [(ngModel)]="searchTerm" />
<div *ngFor="let result of results()">{{ result }}</div>
`,
})
export class SearchComponent {
searchTerm = signal<string>('');
results = signal<string[]>([]);
constructor(private searchService: SearchService) {
// Effect runs when searchTerm changes
effect(() => {
const term = this.searchTerm();
if (term.length > 2) {
this.searchService.search(term).subscribe(results => {
this.results.set(results);
});
} else {
this.results.set([]);
}
});
}
}
Signals vs Observables
When to use each:
// Use Signals for:
// - Component state
// - Derived/computed values
// - Simple reactive values
const count = signal(0);
const double = computed(() => count() * 2);
// Use Observables for:
// - Async operations (HTTP, WebSockets)
// - Complex event streams
// - When you need operators (map, filter, etc.)
this.userService.getUsers().pipe(
map(users => users.filter(u => u.active)),
debounceTime(300)
).subscribe(activeUsers => {
// Handle async data
});
Combining Signals and Observables
Convert between signals and observables:
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { Component } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app-users',
template: `
<div *ngFor="let user of users()">{{ user.name }}</div>
`,
})
export class UsersComponent {
// Convert Observable to Signal
users = toSignal(this.userService.getUsers(), {
initialValue: []
});
// Convert Signal to Observable
count$ = toObservable(this.countSignal);
}
Signal-Based Forms
Use signals with reactive forms:
import { Component, signal, computed } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-user-form',
template: `
<form [formGroup]="form">
<input formControlName="name" />
<input formControlName="email" />
<button [disabled]="!isValid()">Submit</button>
</form>
`,
})
export class UserFormComponent {
form: FormGroup;
isValid = computed(() => this.form.valid);
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
});
}
}
Performance Benefits
Signals provide automatic optimization:
@Component({
selector: 'app-product-list',
template: `
<div *ngFor="let product of filteredProducts()">
{{ product.name }}
</div>
`,
})
export class ProductListComponent {
products = signal<Product[]>([]);
filter = signal<string>('');
// Only recomputes when products or filter change
filteredProducts = computed(() => {
const filterValue = this.filter().toLowerCase();
return this.products().filter(product =>
product.name.toLowerCase().includes(filterValue)
);
});
}
Best Practices
- Use signals for component state: Replace local variables with signals
- Use computed for derived state: Automatic memoization
- Combine with Observables: Use
toSignalandtoObservable - Use effects sparingly: Prefer computed signals when possible
- Signal-based inputs: Use
input()for better type safety - Avoid nested signals: Keep signal structure flat when possible
- Use with OnPush: Signals work excellently with OnPush change detection
Real-World Example
Complete example from production:
import { Component, signal, computed, effect } from '@angular/core';
import { PlayerService } from './player.service';
interface Player {
id: string;
name: string;
position: string;
active: boolean;
}
@Component({
selector: 'app-player-list',
template: `
<input
[(ngModel)]="searchTerm"
(ngModelChange)="searchTerm.set($event)"
placeholder="Search players"
/>
<select
[(ngModel)]="selectedPosition"
(ngModelChange)="selectedPosition.set($event)"
>
<option value="">All Positions</option>
<option value="forward">Forward</option>
<option value="midfielder">Midfielder</option>
</select>
<div *ngFor="let player of filteredPlayers()">
{{ player.name }} - {{ player.position }}
</div>
<p>Total: {{ filteredPlayers().length }}</p>
`,
})
export class PlayerListComponent {
players = signal<Player[]>([]);
searchTerm = signal<string>('');
selectedPosition = signal<string>('');
filteredPlayers = computed(() => {
const term = this.searchTerm().toLowerCase();
const position = this.selectedPosition();
return this.players().filter(player => {
const matchesSearch = player.name.toLowerCase().includes(term);
const matchesPosition = !position || player.position === position;
return matchesSearch && matchesPosition;
});
});
constructor(private playerService: PlayerService) {
effect(() => {
this.playerService.getPlayers().subscribe(players => {
this.players.set(players);
});
});
}
}
Performance Impact
Signals provide:
- Fine-grained reactivity (only affected components update)
- Automatic change detection optimization
- Better performance with OnPush strategy
- Reduced memory overhead compared to Observables
- Simpler mental model for many use cases
Angular Signals represent the future of reactivity in Angular. They complement Observables perfectly, providing a simpler API for many common scenarios while maintaining the power of RxJS for complex async operations.
Keep reading
RxJS Essentials: Most Used Operators and Patterns
Master the most commonly used RxJS operators and patterns for handling async operations, data transformation, and reactive programming in Angular.
NgRx State Management: Enterprise-Ready Angular Architecture
Master NgRx for scalable Angular applications. Learn actions, reducers, effects, and selectors to build maintainable state management solutions.
Angular 18: Latest Features and What's New
Discover the latest features in Angular 18, including improved Signals, new control flow syntax, and enhanced developer experience.