Introduction to RxJS
RxJS (Reactive Extensions for JavaScript) is the library Angular uses to model values that arrive over time — HTTP responses, router events, form changes, WebSocket messages, and user interactions. Instead of imagining a value as a single thing you fetch once, reactive programming treats it as a stream you can listen to, transform, combine, and react to. If you understand observables, you understand a large slice of how Angular actually works under the hood.
What is reactive programming?
Reactive programming is a style of writing code that reacts to changes rather than pulling values on demand. You declare how data should flow and what should happen when new values appear, and the library pushes those values to you as they happen.
The central abstraction is the Observable: a lazy producer of zero, one, or many values over time. An observable does nothing until someone subscribes to it. Each value it emits is delivered to your callback, and the observable can also signal that it has completed (no more values) or errored (something went wrong).
| Concept | Meaning |
|---|---|
| Observable | A source that produces values over time (the stream) |
| Observer | The set of callbacks (next, error, complete) that consume values |
| Subscription | The live connection created by subscribe(); can be torn down |
| Operator | A pure function (map, filter, switchMap, …) that transforms a stream |
| Emission | A single value pushed out of the observable |
Your first observable
You create an observable, then subscribe to start receiving its emissions. The Observer can be a single function (the next handler) or an object with next, error, and complete.
import { Observable } from 'rxjs';
const numbers$ = new Observable<number>((subscriber) => {
subscriber.next(1);
subscriber.next(2);
subscriber.next(3);
subscriber.complete();
});
numbers$.subscribe({
next: (value) => console.log('Got value:', value),
error: (err) => console.error('Error:', err),
complete: () => console.log('Stream complete'),
});
Output:
Got value: 1
Got value: 2
Got value: 3
Stream complete
The trailing
$onnumbers$is a widely used convention signalling “this variable is an observable.” It is not syntax — just a readability hint that keeps streams visually distinct from plain values.
Why Angular leans on RxJS
Angular exposes many of its asynchronous APIs as observables, so reactive programming is not optional knowledge — it is the language Angular speaks. The most common source is HttpClient, whose methods all return observables.
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AsyncPipe } from '@angular/common';
import { Observable } from 'rxjs';
interface User {
id: number;
name: string;
}
@Component({
selector: 'app-user-list',
standalone: true,
imports: [AsyncPipe],
template: `
@if (users$ | async; as users) {
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }}</li>
}
</ul>
} @else {
<p>Loading users…</p>
}
`,
})
export class UserListComponent {
private http = inject(HttpClient);
users$: Observable<User[]> = this.http.get<User[]>('/api/users');
}
The async pipe subscribes to users$ for you, renders each emission, and automatically unsubscribes when the component is destroyed — eliminating an entire class of memory leaks. The new @if/@for control flow reads the stream’s latest value declaratively.
Operators: transforming streams
The real power of RxJS is in operators — small, composable functions you chain inside pipe() to reshape a stream. Operators are pure: they return a new observable and never mutate the source.
import { from } from 'rxjs';
import { filter, map } from 'rxjs';
const result$ = from([1, 2, 3, 4, 5, 6]).pipe(
filter((n) => n % 2 === 0),
map((n) => n * 10),
);
result$.subscribe((value) => console.log(value));
Output:
20
40
60
Here from turns an array into a stream, filter lets only even numbers through, and map multiplies each survivor by ten. Because each operator returns a fresh observable, you can read the pipeline top-to-bottom like a data-processing recipe.
Observables are lazy and (usually) cold
Two properties trip up newcomers. First, observables are lazy: the function you pass to new Observable(...) does not run until subscribe() is called. Second, most observables are cold — each subscriber triggers its own independent execution.
import { defer } from 'rxjs';
const random$ = defer(() => {
const value = Math.random();
return [value];
});
random$.subscribe((v) => console.log('A:', v));
random$.subscribe((v) => console.log('B:', v));
Output:
A: 0.4172
B: 0.8391
Each subscription re-ran the producer, so A and B see different random numbers. When you instead want shared execution that multicasts one result to many subscribers, you reach for Subjects or the share() operator.
Best practices
- Suffix observable variables with
$so streams are instantly recognizable in code review. - Prefer the
asyncpipe in templates over manualsubscribe()— it handles subscription lifecycle for you and prevents leaks. - Keep transformation logic inside
pipe()with pure operators rather than doing work insidesubscribecallbacks. - Remember observables are lazy: nothing runs until subscription, so side effects belong in
tapor the subscriber, not in setup code. - Always have a teardown strategy for manual subscriptions (the
asyncpipe,takeUntilDestroyed, or storing theSubscription). - Reach for
share()/shareReplay()or a Subject when multiple consumers should share one execution instead of triggering it repeatedly.