Angular Signals vs RxJS Observables
- Last updated
- Prerequisites:
- Angular change detection basics
- RxJS Observables and operators
- angular
- signals
- rxjs
- reactivity
- change-detection
- zonejs
Read at your depth
The practical view
A signal holds a value and notifies its consumers when it changes: const count = signal(0); count.set(1); count.update((v) => v + 1); const doubled = computed(() => count() * 2);. In templates you write {{ count() }} — the () makes it lazy and automatically tracked. An Observable is a stream: const clicks$ = fromEvent(button, 'click'); clicks$.pipe(map(...), filter(...)).subscribe(...). Practical difference: signals are eager-by-value and synchronous; observables are lazy, async, and push values through a pipeline over time. Signals are the right tool for state owned by a component; observables remain right for streams of events, WebSocket messages, and debounced input where time-based operators are needed.
The same idea in other frameworks
const [count, setCount] = useState(0); // setCount triggers re-render
const doubled = count * 2;const count = signal(0);
const doubled = computed(() => count() * 2);const count = ref(0);
const doubled = computed(() => count.value * 2);let count = $state(0);
let doubled = $derived(count * 2);Legacy vs modern
Shared value via Subject + async pipe vs signal with computed
Both approaches keep a UI in sync, but the signal version is synchronous, tree-local, and granular; the RxJS version is async and needs subscription lifecycle management.
count$ = new BehaviorSubject(0); // ❌ OLD: BehaviorSubject + async pipe → async, needs subscription lifecycle
increment() { this.count$.next(this.count$.value + 1); }count = signal(0); // ✅ NEW: signal() + computed() → synchronous, granular CD
increment() { this.count.update((v) => v + 1); }Interview gotchas
Context
Interviewers want to see that you do not treat signals as a universal replacement; they probe your judgment about async boundaries.
The mechanical answer
Signals are synchronous pull-based state — perfect for values that exist at a point in time. RxJS earns its place when values arrive over time or need time-based operators: debouncing user input, websocket streams, retry/backoff on HTTP, combining multiple async sources with combineLatest/zip, or cancellation semantics. The classic hybrid is: keep the external stream in RxJS (toSignal to enter the signal world), then derive everything downstream with computed(). Trying to model a hot event stream purely with signals means hand-rolling debounce/timing logic that operators already implement and test.
Trap
The naive answer is 'signals replace observables everywhere' or 'subscribe in a component'. Real-world Angular code still uses RxJS for inter-service messaging and async pipelines, and the Angular team explicitly positions signals as state, not streams. Saying 'signals are synchronous and observables are asynchronous' without connecting it to when each property matters (state vs event streams) reads as memorized, not understood.
Context
This separates developers who understand the reactivity graph from those who think signals are just syntax sugar for subjects.
The mechanical answer
Reading count() inside a template registers the current template effect as a consumer of that signal node. When the signal's version bumps, that consumer is marked dirty and only its owning view re-checks its bindings. Before signals, Angular had to assume any async activity anywhere might have changed anything, so Zone.js monkey-patched every async API (setTimeout, addEventListener, Promise) to notify Angular to run change detection over the whole tree, with OnPush as an optimization. Signals replace that blanket assumption with an explicit dependency graph, which is why zoneless Angular 18+ can run change detection only where needed.
Trap
A tempting but wrong claim is 'signals make change detection obsolete'. Change detection still runs — it just becomes granular and event-driven. Another trap: assuming computed() re-evaluates on every read; it memoizes and only re-evaluates when an upstream dependency changes, which is exactly the lazy pull behavior interviewers look for.