const count = signal(0);
const doubled = computed(() => count() * 2);
count.set(5);
console.log(doubled()); // 10Angular Signals, introduced in version 16, represent a fundamental shift in how Angular handles reactivity. If you have been working with RxJS Observables and zone.js change detection, Signals offer a simpler, more predictable alternative for managing component state.
#What Are Signals?
A Signal is a wrapper around a value that notifies consumers when that value changes. Unlike Observables, Signals are synchronous and always have a current value.
import { signal } from '@angular/core';
// Create a signal with an initial value
const count = signal(0);
// Read the current value
console.log(count()); // 0
// Update the value
count.set(5);
console.log(count()); // 5The key difference from traditional variables is that Angular tracks when you read a Signal and automatically updates the UI when that Signal changes.
#Why Signals Matter
#Fine-Grained Reactivity
With zone.js, Angular checks the entire component tree for changes. Signals enable fine-grained updates where only the specific DOM nodes that depend on a changed Signal get updated.
@Component({
template: `
<h1>{{ title() }}</h1>
<p>Count: {{ count() }}</p>
<button (click)="increment()">+1</button>
`
})
export class CounterComponent {
title = signal('My Counter');
count = signal(0);
increment() {
this.count.update(c => c + 1);
// Only the <p> element updates, not the <h1>
}
}#Simpler Mental Model
Observables require understanding subscriptions, operators, and memory management. Signals are straightforward: set a value, read a value.
// RxJS approach
private countSubject = new BehaviorSubject(0);
count$ = this.countSubject.asObservable();
increment() {
this.countSubject.next(this.countSubject.value + 1);
}
// Signals approach
count = signal(0);
increment() {
this.count.update(c => c + 1);
}#Core Signal APIs
#signal() - Writable Signals
Create a mutable signal that you can read and write.
const name = signal('Alice');
// Read
name(); // 'Alice'
// Write (replace value)
name.set('Bob');
// Write (based on previous value)
name.update(current => current.toUpperCase());#computed() - Derived Values
Create a read-only signal that automatically updates when its dependencies change.
import { signal, computed } from '@angular/core';
const firstName = signal('John');
const lastName = signal('Doe');
// Automatically updates when firstName or lastName changes
const fullName = computed(() => `${firstName()} ${lastName()}`);
console.log(fullName()); // 'John Doe'
firstName.set('Jane');
console.log(fullName()); // 'Jane Doe'Computed signals are lazy. They only recalculate when you read them and a dependency has changed.
#effect() - Side Effects
Run code whenever a signal changes. Effects are useful for logging, syncing with localStorage, or making API calls.
import { signal, effect } from '@angular/core';
const theme = signal('light');
effect(() => {
document.body.className = theme();
localStorage.setItem('theme', theme());
});
// Changing the signal triggers the effect
theme.set('dark');Effects run at least once during creation and then whenever any signal they read changes.
#Signals with Objects and Arrays
When working with objects or arrays, use the update method to maintain immutability.
interface User {
name: string;
age: number;
}
const user = signal<User>({ name: 'Alice', age: 30 });
// Update a property
user.update(current => ({ ...current, age: 31 }));
// Arrays
const items = signal<string[]>(['apple', 'banana']);
// Add item
items.update(current => [...current, 'cherry']);
// Remove item
items.update(current => current.filter(item => item !== 'banana'));#Input Signals (Angular 17.1+)
Angular 17.1 introduced signal-based inputs, replacing the traditional @Input() decorator.
import { Component, input } from '@angular/core';
@Component({
selector: 'app-greeting',
template: `<h1>Hello, {{ name() }}!</h1>`
})
export class GreetingComponent {
// Required input
name = input.required<string>();
// Optional input with default
greeting = input('Hello');
}Signal inputs are read-only and integrate seamlessly with computed signals.
export class UserCardComponent {
firstName = input.required<string>();
lastName = input.required<string>();
// Derived from inputs
fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
}#When to Use Signals vs RxJS
Signals and RxJS serve different purposes. Here is a practical guide:
Use Signals for:
- Component state (counters, form values, UI toggles)
- Derived/computed values
- Simple parent-child communication
- Replacing BehaviorSubject for synchronous state
Use RxJS for:
- HTTP requests and async operations
- Complex event streams (debounce, throttle, merge)
- WebSocket connections
- When you need operators like switchMap, combineLatest, or retry
You can convert between them when needed:
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
// Observable to Signal
const data = toSignal(this.http.get('/api/data'));
// Signal to Observable
const count = signal(0);
const count$ = toObservable(count);#Migration Tips
If you are adding Signals to an existing project:
-
Start with new components. Use Signals for any new features you build.
-
Identify BehaviorSubject candidates. Simple synchronous state held in BehaviorSubjects often converts cleanly to Signals.
-
Keep RxJS for async. Do not try to replace HTTP calls or complex streams with Signals.
-
Use computed for derived state. Replace getter methods that combine multiple values with computed signals.
-
Test incrementally. Signals work alongside zone.js, so you can migrate gradually.
#Performance Considerations
Signals improve performance in several ways:
- Reduced change detection cycles. Only affected components update.
- Lazy computation. Computed signals only recalculate when read.
- No subscription management. Unlike Observables, you do not need to unsubscribe.
For most applications, simply using Signals will improve performance without any optimization effort. For advanced cases, Angular 18+ supports zoneless change detection with Signals.
#Summary
Angular Signals provide a simpler, more efficient approach to reactive state management. They reduce boilerplate, improve performance through fine-grained reactivity, and offer a gentler learning curve than RxJS for common use cases.
Start by using Signals for component state in new features, and gradually adopt them across your application as you become comfortable with the API.