The Problem Signals Solve
Angular's traditional change detection runs top-down after every async event, checking the entire component tree. This is expensive at scale. RxJS handles async streams elegantly, but brings considerable boilerplate for simple synchronous state like a counter, a toggle, or a form value.
Signals are Angular's answer: lightweight, synchronous, and automatically tracked. When a signal's value changes, only the parts of the template that depend on it re-render — nothing else.
The Core API
signal() — writable state
import { signal } from '@angular/core';
const count = signal(0);
// Read — call it like a function
console.log(count()); // 0
// Write
count.set(5);
count.update(val => val + 1); // 6
computed() — derived state
Computed signals re-evaluate automatically when any of their dependencies change:
import { signal, computed } from '@angular/core';
const price = signal(100);
const quantity = signal(3);
const total = computed(() => price() * quantity());
console.log(total()); // 300
price.set(120);
console.log(total()); // 360 — recalculated automatically
effect() — side effects
Run code whenever a signal's value changes:
import { signal, effect } from '@angular/core';
const theme = signal('dark');
effect(() => {
document.body.setAttribute('data-theme', theme());
});
theme.set('light'); // triggers the effect immediately
Effects must be created inside an injection context (a constructor, or via
runInInjectionContext). They clean themselves up automatically when the component is destroyed.
A Practical Example: Shopping Cart
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-cart',
standalone: true,
template: `
<p>Items: {{ itemCount() }}</p>
<p>Total: ${{ total() | number:'1.2-2' }}</p>
<button (click)="addItem()">Add Widget</button>
`
})
export class CartComponent {
items = signal<{ name: string; price: number }[]>([]);
itemCount = computed(() => this.items().length);
total = computed(() =>
this.items().reduce((sum, item) => sum + item.price, 0)
);
addItem() {
this.items.update(list => [...list, { name: 'Widget', price: 9.99 }]);
}
}
No ChangeDetectorRef, no markForCheck(), no BehaviorSubject — the template just works.
Signals vs RxJS
These aren't competing technologies — they're complementary:
- Use Signals for component state, derived values, and anything synchronous.
- Use RxJS for HTTP calls, WebSockets, debouncing, and complex async flows.
- Bridge them with
toSignal(observable$)andtoObservable(signal)from@angular/core/rxjs-interop.
Conclusion
Signals are Angular's most significant DX improvement since standalone components. For new features, reach for signals first — they're simpler to reason about, easier to test, and faster at runtime. Keep RxJS where async streams genuinely need it, and use the interop utilities to bridge the two worlds cleanly.