Observables & RxJS in Angular
RxJS (Reactive Extensions for JavaScript) is the backbone of asynchronous programming in Angular. HttpClient, the Router, FormControl.valueChanges, and many other Angular APIs all return Observables.
An Observable is a lazy, cancellable stream of values over time. It can emit zero, one, or many values, and it completes (or errors) eventually.
Observable vs Promise
Feature | Promise | Observable |
|---|---|---|
Values emitted | Exactly one | Zero, one, or many |
Lazy? | Eager (runs immediately) | Lazy (runs on subscribe) |
Cancellable? | No | Yes (unsubscribe) |
Operators | Limited (.then/.catch) | Rich operator library (map, filter, ...) |
Multi-value streams | Not supported | Native (WebSocket, setInterval, events) |
Angular usage | Works but limited | First-class throughout Angular |
Creating Observables
RxJS provides many creation functions. These are the most common.
import { Observable, of, from, interval, fromEvent, timer } from 'rxjs';
// --- of() — emit a fixed set of values synchronously
const numbers$ = of(1, 2, 3);
numbers$.subscribe(console.log); // 1, 2, 3
// --- from() — convert a Promise or array to an Observable
const promise$ = from(fetch('/api/data').then(r => r.json()));
const array$ = from([10, 20, 30]);
// --- interval() — emit a number every N milliseconds
const tick$ = interval(1000); // 0, 1, 2, 3... every second
// --- fromEvent() — DOM events as an Observable
const clicks$ = fromEvent(document, 'click');
// --- timer() — emit once after a delay, or repeatedly
const delayed$ = timer(3000); // emits 0 after 3 seconds
const delayed2$ = timer(0, 1000); // starts immediately, repeats every second
// --- new Observable() — full manual control
const custom$ = new Observable<number>((subscriber) => {
subscriber.next(1);
subscriber.next(2);
subscriber.complete();
});Subscribing and Unsubscribing
import { interval } from 'rxjs';
const counter$ = interval(1000);
// subscribe() returns a Subscription object
const subscription = counter$.subscribe({
next: (value) => console.log('Value:', value),
error: (err) => console.error('Error:', err),
complete: () => console.log('Completed'),
});
// Unsubscribe after 5 seconds to prevent memory leaks
setTimeout(() => {
subscription.unsubscribe();
console.log('Unsubscribed!');
}, 5000);Managing Subscriptions in Angular Components
Option 1: async pipe (recommended) — Angular automatically subscribes and unsubscribes.
// Using async pipe — no manual subscription
@Component({
template: `
@if (user$ | async; as user) {
<h1>{{ user.name }}</h1>
}
`,
})
export class UserComponent {
user$ = this.userService.getUser(1);
}Option 2: takeUntilDestroyed (Angular 16+) — auto-unsubscribes on component destroy.
import { Component, OnInit, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
@Component({ selector: 'app-timer', standalone: true, template: `{{ count }}` })
export class TimerComponent implements OnInit {
count = 0;
private destroyRef = inject(DestroyRef); // injected automatically
ngOnInit(): void {
interval(1000)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => this.count++);
}
}Option 3: takeUntil with a Subject — classic pattern for older Angular.
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({ selector: 'app-example', standalone: true, template: '' })
export class ExampleComponent implements OnDestroy {
private destroy$ = new Subject<void>();
init(): void {
someObservable$
.pipe(takeUntil(this.destroy$))
.subscribe(handleValue);
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}Cold vs Hot Observables
Cold Observable | Hot Observable | |
|---|---|---|
Definition | Produces data for each subscriber independently | Shares a single data source among all subscribers |
Example | HTTP request, of(), from() | fromEvent(), Subject, WebSocket |
Side effect | Each subscribe triggers a new execution | Execution started regardless of subscribers |
Common use | HTTP calls, one-off data | Events, shared state, broadcasts |
Core RxJS Operators in Action
import { of, from } from 'rxjs';
import { map, filter, tap, take, debounceTime, distinctUntilChanged } from 'rxjs/operators';
// map — transform each value
of(1, 2, 3).pipe(
map(x => x * 2)
).subscribe(console.log); // 2, 4, 6
// filter — only pass through matching values
of(1, 2, 3, 4, 5).pipe(
filter(x => x % 2 === 0)
).subscribe(console.log); // 2, 4
// tap — side effects without transformation
of(1, 2, 3).pipe(
tap(x => console.log('Before map:', x)),
map(x => x * 10),
tap(x => console.log('After map:', x))
).subscribe();
// take — complete after N values
interval(500).pipe(take(3)).subscribe(console.log); // 0, 1, 2 then completes
// debounceTime — wait for silence before emitting (search boxes)
searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged()
).subscribe(searchTerm => this.search(searchTerm));Higher-Order Observables (switchMap, mergeMap, concatMap)
These operators handle the common case where you need to map a value to a new Observable (e.g., making an HTTP request for each route change).
import { switchMap, mergeMap, concatMap } from 'rxjs/operators';
// switchMap — cancel previous inner Observable on new emission (search, navigation)
searchControl.valueChanges.pipe(
debounceTime(300),
switchMap(term => this.http.get<Result[]>(`/api/search?q=${term}`))
).subscribe(results => this.results = results);
// mergeMap — subscribe to all inner Observables concurrently
from([1, 2, 3]).pipe(
mergeMap(id => this.http.get<User>(`/api/users/${id}`))
).subscribe(user => console.log(user)); // all 3 requests fire in parallel
// concatMap — wait for each inner Observable to complete before starting the next
from([1, 2, 3]).pipe(
concatMap(id => this.http.get<User>(`/api/users/${id}`))
).subscribe(user => console.log(user)); // sequential: 1 → 2 → 3Operator | Behaviour | Best For |
|---|---|---|
switchMap | Cancels previous, starts new | Search, route changes |
mergeMap | All run concurrently | Parallel independent requests |
concatMap | Queues — one at a time in order | Sequential operations, file uploads |
exhaustMap | Ignores new values while current runs | Login button, form submit |
Combining Observables
import { combineLatest, forkJoin, zip, merge } from 'rxjs';
// forkJoin — like Promise.all, emits when ALL complete
forkJoin({
users: this.http.get<User[]>('/api/users'),
posts: this.http.get<Post[]>('/api/posts'),
}).subscribe(({ users, posts }) => {
console.log('Users:', users);
console.log('Posts:', posts);
});
// combineLatest — emits when ANY source emits, with latest values from all
combineLatest([filter$, sort$, page$]).pipe(
switchMap(([filter, sort, page]) =>
this.http.get<Product[]>(`/api/products?filter=${filter}&sort=${sort}&page=${page}`)
)
).subscribe(products => this.products = products);
// merge — interleave emissions from multiple Observables
merge(click$, keydown$).subscribe(event => console.log(event));Error Handling
import { catchError, retry, of, throwError, EMPTY } from 'rxjs';
this.http.get<User[]>('/api/users').pipe(
retry(2), // retry twice before error propagates
catchError((err) => {
if (err.status === 404) {
return of([]); // return empty array as fallback
}
if (err.status === 401) {
this.router.navigate(['/login']);
return EMPTY; // cancel the stream
}
return throwError(() => err); // re-throw for higher-level handling
})
).subscribe({
next: (users) => this.users = users,
error: (err) => this.errorMessage = err.message,
});toSignal — Bridging Observables and Signals
Angular 16+ provides toSignal() and toObservable() to bridge the reactive worlds.
import { Component, inject } from '@angular/core';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { signal } from '@angular/core';
import { switchMap } from 'rxjs/operators';
@Component({
selector: 'app-search',
standalone: true,
template: `
<input (input)="term.set($event.target.value)" placeholder="Search..." />
@for (result of results(); track result.id) {
<div>{{ result.name }}</div>
}
`,
})
export class SearchComponent {
private http = inject(HttpClient);
term = signal('');
// Convert signal to Observable, pipe through HTTP, back to signal
results = toSignal(
toObservable(this.term).pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.http.get<Result[]>(`/api/search?q=${term}`))
),
{ initialValue: [] }
);
}Best Practices
Always unsubscribe — use async pipe, takeUntilDestroyed, or takeUntil
Use the async pipe in templates for automatic subscription management
Prefer switchMap for search and navigation to cancel stale requests
Use forkJoin for parallel HTTP calls that all need to complete before rendering
Never subscribe inside subscribe — use switchMap/mergeMap/concatMap instead
Use catchError to handle errors gracefully rather than letting them propagate
Use toSignal() when bridging Observables into signal-based components
Name Observable variables with a $ suffix for clarity (e.g., user$, products$)
Summary
RxJS Observables are central to Angular's asynchronous architecture. Understanding lazy evaluation, subscription management, and higher-order mapping operators (switchMap, mergeMap, concatMap) is essential for building reactive Angular applications. The toSignal/toObservable bridge makes it seamless to mix the Observable and signal paradigms in modern Angular 16+ applications.