Signals were introduced in Angular 16 (as a developer preview) and became stable in Angular 17+. They represent a fundamental shift in Angular’s reactivity model, offering a simpler way to manage state and reactivity in applications.
Key Signal Types Introduced
1. Writable Signals (signal())
The basic building block for reactive state:
import { signal } from '@angular/core'; count = signal(0); // Initial value
2. Computed Signals (computed())
Derived values that automatically update:
doubleCount = computed(() => this.count() * 2);
3. Effects (effect())
For side effects when signals change:
effect(() => { console.log('Count changed:', this.count()); });
4. Input Signals (input())
(Introduced in Angular 17+)
name = input.required<string>(); // Required input count = input(0); // Optional with default
5. Model Signals (model())
(Introduced in Angular 17+ for two-way binding)
value = model('default');
How Signals Work
Reactivity Graph
Signals create a dependency graph:
- When you read a signal (
signal()), it tracks the consumer - When the signal value changes, it notifies all dependents
- Computed values and effects re-evaluate automatically
Change Detection Mechanism
- Granular Updates: Only components that use changed signals are marked for update
- Glitch-Free: Angular ensures no intermediate states are shown
- No Zone.js Required: Signals can work without zone.js (future direction)
Performance Characteristics
- Lazy Evaluation: Computed values only recalculate when needed
- Memoization: Same signal values don’t trigger recomputations
- Cleanup: Automatic dependency cleanup when contexts are destroyed
Core API Details
Signal Operations
const count = signal(0); // Set value directly count.set(5); // Update based on previous value count.update(v => v + 1); // Mutate object/array contents const items = signal([{id: 1}]); items.mutate(arr => arr.push({id: 2}));
Computed Deep Dive
const a = signal(1); const b = signal(2); // Only recomputes when a or b changes const sum = computed(() => a() + b()); // Computed signals are lazily evaluated console.log(sum()); // First read triggers computation
Effect Behavior
effect(() => { // Runs immediately and whenever count changes console.log('Current count:', count()); }, { allowSignalWrites: true }); // Optionally allow writing signals
Integration with Angular Features
Template Binding
<p>{{ count() }}</p> <button (click)="count.update(c => c + 1)">Increment</button>
Component Communication
// Parent component items = signal([...]); // Child component @Input({ required: true }) items!: Signal<Item[]>;
RxJS Interop
import { toSignal, toObservable } from '@angular/core/rxjs-interop'; // Observable → Signal data$ = this.http.get('/api/data'); data = toSignal(this.data$, { initialValue: [] }); // Signal → Observable count$ = toObservable(this.count);
Advanced Patterns
Signal-Based Services
@Injectable({ providedIn: 'root' }) export class CartService { private items = signal<CartItem[]>([]); totalItems = computed(() => this.items().length); addItem(item: CartItem) { this.items.update(items => [...items, item]); } }
Signal Queries (Angular 17+)
// Replaces ViewChild with signals divRef = viewChild<ElementRef>('myDiv'); divRefs = viewChildren<ElementRef>('items');
Migration Considerations
- Gradual Adoption: Can be introduced alongside existing RxJS code
- Performance Benefits: Especially noticeable in large applications
- Future-Proofing: Signals are the foundation for Angular’s future reactivity
Signals represent Angular’s evolution toward simpler reactivity while maintaining performance. They work alongside (not replace) RxJS, with each having their optimal use cases.