Angular Signals landed as a developer preview in Angular 16 and became stable in Angular 17. They represent the most significant shift in Angular's reactivity model since the introduction of RxJS Observables, and they solve a very specific problem: most component state does not need the full power of an Observable stream.
The Problem Signals Solve
In a typical Angular component, you might write:
@Component({
template: `<p>{{ count }}</p>`,
})
export class CounterComponent {
count = 0;
increment() {
this.count++;
}
}
This works, but Angular uses Zone.js change detection to know when to re-render — which means running change detection on the entire component tree on every microtask. Signals let Angular track exactly which template expressions depend on which values, enabling fine-grained, zone-free change detection.
Creating and Reading Signals
import { signal, computed, effect } from '@angular/core';
const count = signal(0);
// Read the value
console.log(count()); // 0
// Write
count.set(1);
count.update(v => v + 1);
Signals are functions — you call them to read their current value. This is intentional: it makes reads explicit and traceable by the Angular runtime.
Computed Signals
computed() creates a derived signal that re-evaluates lazily when its dependencies change:
const doubled = computed(() => count() * 2);
console.log(doubled()); // 2 after count is 1
Angular caches the computed value and only recalculates when one of the signals it read has changed. There is no distinctUntilChanged to add — this is built in.
Effects
effect() runs a side effect whenever its dependencies change:
effect(() => {
console.log(`Count is now: ${count()}`);
});
Effects are useful for logging, syncing state to localStorage, or triggering non-Angular side effects. They should not be used to update other signals — use computed() for derived state instead.
Signals vs Observables
Signals and Observables are complementary, not competing. The rule of thumb:
| Use Signals for... | Use Observables for... | |---|---| | Component state | HTTP requests | | Derived/computed values | Event streams | | Template bindings | Debounced input | | Simple two-way binding | Complex async pipelines |
Angular provides toSignal() and toObservable() to bridge the two worlds when needed.
Signal-Based Inputs (Angular 17.1+)
Angular 17.1 introduced signal-based inputs, replacing @Input() decorators:
@Component({ ... })
export class CardComponent {
title = input.required<string>();
subtitle = input('');
}
Signal inputs are read-only signals — you cannot .set() them from inside the component. They work naturally with computed():
displayTitle = computed(() => this.title().toUpperCase());
Conclusion
Signals are the future of state management in Angular. They are simpler to reason about than Observables for local component state, they enable better performance through fine-grained change detection, and they reduce the boilerplate around Zone.js. Start adopting them for new components today — the migration path from existing code is gradual and well-supported.