Signals vs RxJS Observables
Modern Angular applications have two major reactive primitives available: signals, introduced as a first-class reactivity system, and Observables from RxJS, which have been central to Angular since its early versions (particularly for HttpClient, the Router, and Reactive Forms). They solve overlapping but distinct problems, and understanding their differences is essential for writing idiomatic, performant Angular code and for knowing which one to reach for in a given situation.
Cricket analogy: Angular's two reactive primitives are like a cricket team having both a live scoreboard (signals, giving the instant current score) and a ball-by-ball radio commentary feed (Observables, a stream of events over time) — both describe the match, but you reach for different one depending on whether you need the current total or the unfolding sequence of deliveries.
Fundamental differences
A signal is a synchronous container for a single current value; reading it always gives you the latest value immediately, with no subscription required. An Observable, by contrast, models a stream of values distributed over time — it can emit zero, one, or many values (synchronously or asynchronously), and you only receive values by subscribing, at which point you must also remember to unsubscribe to avoid memory leaks. Signals are pull-based (you read the current value on demand) whereas Observables are push-based (values are pushed to subscribers as they occur).
Cricket analogy: A signal is like glancing at the current scoreboard total — always the latest number, no need to 'tune in' — while an Observable is like a radio commentary broadcast that only delivers ball-by-ball updates to those actively listening in, and you must remember to switch off the radio (unsubscribe) when you leave the ground or the batteries drain needlessly.
import { signal, computed } from '@angular/core';
import { BehaviorSubject, map } from 'rxjs';
// Signal: read synchronously, no subscription needed
const count = signal(0);
const doubled = computed(() => count() * 2);
console.log(doubled()); // 0, immediately
count.set(5);
console.log(doubled()); // 10, immediately
// Observable: must subscribe to receive values
const count$ = new BehaviorSubject(0);
const doubled$ = count$.pipe(map(v => v * 2));
doubled$.subscribe(v => console.log(v)); // 0
count$.next(5); // triggers subscriber -> 10When to prefer each
Signals are the better default for local component and application state: form field values, UI toggles, counters, derived display values, and anything driving template rendering, because Angular's change detection can respond to signal reads with fine-grained precision and they eliminate the boilerplate and leak risk of manual subscriptions. RxJS remains the stronger tool for genuinely asynchronous, event-based, or multi-step operations: HTTP requests, WebSocket streams, debounced search inputs, complex operator pipelines (retry, switchMap, combineLatest), and coordinating multiple asynchronous sources with precise timing control.
Cricket analogy: Signals are the better default for a scorer's live tally sheet, over/under counters, and toggle switches like 'powerplay active' since the scoreboard operator just needs fine-grained instant reads; RxJS remains stronger for the genuinely event-driven ball-by-ball radio feed, DRS review pipelines with retry logic, and coordinating multiple simultaneous camera-angle streams with precise timing.
Interoperability
Angular provides the @angular/core/rxjs-interop package with toSignal() and toObservable() to bridge the two worlds. toSignal() converts an Observable into a signal, letting templates read the latest emitted value synchronously without an async pipe or manual subscription; toObservable() does the reverse, turning a signal into an Observable stream so it can participate in RxJS operator chains. This interop is especially common when consuming HttpClient responses (which return Observables) as signals inside a component.
Cricket analogy: toSignal() is like a scorer converting the radio commentary broadcast into a single always-current scoreboard number so the stadium screen can just display the latest score without anyone manually tuning in each ball; toObservable() is the reverse — turning that scoreboard number back into a broadcastable feed other commentary booths can subscribe to, common when the live score originally comes from an HTTP-polled API.
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-user-profile',
standalone: true,
template: `@if (user(); as u) { <p>{{ u.name }}</p> }`,
})
export class UserProfileComponent {
private http = inject(HttpClient);
// Converts the HTTP Observable into a signal readable in the template
user = toSignal(this.http.get<{ name: string }>('/api/me'), { initialValue: null });
}A useful mental model: signals are Angular's answer to 'what is the current state right now,' similar to React's useState/useSignal-style primitives, while Observables are Angular's answer to 'what is happening over time,' similar to event streams. Neither replaces the other outright — Angular's direction is to use signals for state and RxJS for asynchronous event orchestration, joined by the interop layer.
A common pitfall is forgetting that toSignal() requires an initial value (or must tolerate undefined) for Observables that don't emit synchronously, and that calling toSignal() or toObservable() outside an injection context requires manually passing an injector. Another pitfall is trying to replace every Observable with a signal — operators like debounceTime, switchMap, and retry have no direct signal equivalent and are still best expressed with RxJS.
- Signals are synchronous, pull-based containers for a current value; Observables are push-based streams of values over time.
- Signals require no subscription or unsubscription; Observables must be subscribed to and, in most cases, explicitly unsubscribed from.
- Prefer signals for local component/application state and derived display values.
- Prefer RxJS for asynchronous operations, event streams, and complex operator pipelines like debounce, retry, and switchMap.
- toSignal() and toObservable() from @angular/core/rxjs-interop bridge the two systems.
- HttpClient, the Router, and Reactive Forms still return Observables, so RxJS knowledge remains essential even in a signals-first codebase.
Practice what you learned
1. Which statement best describes the difference in access model between signals and Observables?
2. What is the purpose of toSignal()?
3. Which of these is still typically represented as an Observable in modern Angular, even in a signals-first app?
4. Why must Observables typically be unsubscribed from, unlike signals?
5. Which task is best suited to RxJS rather than signals?
Was this page helpful?
You May Also Like
computed() and effect()
Learn how computed() derives memoized reactive values from signals, and how effect() runs side effects automatically whenever the signals it reads change.
Introduction to Signals
Understand Angular Signals — the reactive primitive that wraps a value and notifies dependents on change, forming the foundation of Angular's modern change-detection model.
RxJS Operators for HTTP (map, switchMap, catchError)
Master the core RxJS operators — map, switchMap, catchError, and their flattening cousins — used to transform, chain, and recover from HTTP requests.
HttpClient and Fetching Data
Learn how Angular's HttpClient issues typed, Observable-based HTTP requests, and how to provide it in a standalone application.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics