A practical RxJS guide focused on the most important operators, with beginner-friendly explanations, real use cases, Angular patterns, examples, traps, and practice questions.
pipe() -> subscriber or async pipe.pluck with map plus optional property access, prefer configurable retry over retryWhen for ordinary backoff, and replace publish with share or connectable.| Category | Important operators | Purpose |
|---|---|---|
| Creation | of, from, fromEvent, interval, timer, defer, throwError | Create Observable sources. |
| Transformation | map, scan, reduce, bufferTime, pairwise | Change emitted values; use map(value => value?.property) for property selection. |
| Filtering | filter, take, first, last, skip, distinctUntilChanged, debounceTime, throttleTime | Control which values pass through. |
| Combination | combineLatest, withLatestFrom, forkJoin, zip, merge, concat, race | Work with multiple streams. |
| Flattening | switchMap, mergeMap, concatMap, exhaustMap | Map a value to another Observable and flatten the result. |
| Error and retry | catchError, retry({ count, delay }), finalize | Recover, retry with a limit and delay, and clean up. |
| Multicasting | share, shareReplay, connectable | Share subscriptions, replay selected values, or control connection timing. |
| Operator | Explanation | Example use |
|---|---|---|
of | Creates an Observable from fixed values. | Return fallback data such as of([]). |
from | Creates an Observable from array, promise, iterable, or similar source. | Convert a Promise or array into a stream. |
fromEvent | Creates a stream from DOM events. | Listen to clicks, input, scroll, or keyup. |
interval | Emits numbers repeatedly after a fixed interval. | Polling, timer ticks, live counters. |
timer | Emits after a delay, optionally repeatedly. | Delayed action or scheduled polling. |
defer | Creates a fresh Observable factory for each subscription. | Run current-time logic only when subscribed. |
throwError | Creates an Observable that errors. | Return an error from inside an operator. |
import { of, from, fromEvent, interval, timer, defer } from 'rxjs';
of('Angular', 'React', 'Vue');
from(fetch('/api/users'));
fromEvent(document, 'click');
interval(1000);
timer(2000);
defer(() => of(new Date().toISOString()));
| Operator | Explanation | Example use |
|---|---|---|
map | Transforms each emitted value. | Convert API response into view model. |
map(value => value?.profile?.name) | Selects a nested property with optional chaining. | Modern replacement for deprecated pluck('profile', 'name'). |
scan | Accumulates state over time and emits each step. | Counter, reducer-style state, running totals. |
reduce | Accumulates all values and emits once on completion. | Final sum after finite stream completes. |
pairwise | Emits previous and current value together. | Compare route, scroll, or form changes. |
bufferTime | Collects values for a time window. | Batch frequent events. |
toArray | Collects all values into an array when complete. | Collect results from finite stream. |
users$ = this.http.get<UserDto[]>('/api/users').pipe(
map(users => users.map(user => ({
id: user.id,
label: `${user.firstName} ${user.lastName}`,
active: user.status === 'ACTIVE'
})))
);
| Operator | Explanation | Use when |
|---|---|---|
filter | Passes values matching a condition. | Ignore invalid form values. |
take | Takes a fixed number of values, then completes. | Read first 1 or first N values. |
takeUntil | Completes when another Observable emits. | Cleanup on destroy or cancellation signal. |
first | Emits the first matching value. | Need first valid event. |
skip | Ignores the first N values. | Ignore initial default state. |
distinctUntilChanged | Skips same consecutive value. | Do not repeat search for same term. |
debounceTime | Waits for quiet time before emitting. | Search box after typing stops. |
throttleTime | Emits at most once per time window. | Button click or scroll rate limiting. |
auditTime | Emits latest value after each time window. | UI updates during scroll/resize. |
sampleTime | Samples latest value at intervals. | Periodic snapshot of frequent events. |
searchResults$ = fromEvent<InputEvent>(searchInput, 'input').pipe(
map(event => (event.target as HTMLInputElement).value.trim()),
filter(term => term.length >= 2),
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.api.search(term))
);
| Operator | Explanation | Best use |
|---|---|---|
combineLatest | Combines latest values from multiple streams after each has emitted. | View model from filters, sort, page, and data. |
withLatestFrom | Main stream emits while sampling latest values from others. | Button click uses latest form state. |
forkJoin | Waits for all inner Observables to complete, then emits final values. | Parallel HTTP calls needed once. |
zip | Pairs values by emission order. | Step-by-step pair matching. |
merge | Runs streams together and emits as values arrive. | Combine multiple event sources. |
concat | Runs streams one after another. | Sequence tasks in order. |
race | Uses the stream that emits first. | Timeout or fastest source wins. |
viewModel$ = combineLatest([
users$,
searchTerm$,
selectedRole$
]).pipe(
map(([users, term, role]) => ({
users: users.filter(user =>
user.name.includes(term) && (!role || user.role === role)
),
term,
role
}))
);
| Operator | Behavior | Best use | Avoid when |
|---|---|---|---|
switchMap | Cancels previous inner Observable and switches to latest. | Search, route param HTTP calls, latest result wins. | Every request must finish. |
mergeMap | Runs inner Observables concurrently. | Independent writes, parallel requests, event fan-out. | Order matters or concurrency must be limited. |
concatMap | Queues inner Observables and runs one at a time. | Ordered saves, sequential jobs, queue behavior. | Latest request should cancel old one. |
exhaustMap | Ignores new source values while inner Observable is active. | Login, payment, submit button duplicate prevention. | User should be able to cancel and start a newer request. |
saveClicks$ = fromEvent(saveButton, 'click').pipe(
exhaustMap(() => this.api.saveForm(this.form.value).pipe(
catchError(error => {
this.toast.error('Save failed');
return EMPTY;
})
))
);
| Operator | Explanation | Example use |
|---|---|---|
tap | Runs side effects without changing value. | Logging, analytics, debugging. |
delay | Delays emissions. | Demo loading, backoff sketch. |
timeout | Errors if source takes too long. | Fail slow request with fallback. |
startWith | Emits an initial value before source values. | Initial loading or default UI state. |
endWith | Emits value after source completes. | Completion marker. |
defaultIfEmpty | Emits fallback if source completes without value. | No result fallback. |
isEmpty | Emits whether source had no values. | Empty-state detection. |
| Operator | Explanation | Important rule |
|---|---|---|
catchError | Catches an error and returns a replacement Observable. | Always return an Observable. |
retry({ count, delay }) | Retries a failed source with an explicit limit and fixed or calculated delay. | Retry only idempotent operations; use a delay callback when status-aware backoff is required. |
finalize | Runs cleanup on complete, error, or unsubscribe. | Good for loading flags. |
users$ = this.http.get<User[]>('/api/users').pipe(
retry({ count: 2, delay: 500 }),
catchError(error => {
this.logger.error(error);
return of([]);
}),
finalize(() => this.loading.set(false))
);
| Tool | Explanation | Use carefully |
|---|---|---|
Subject | Manual multicast source. | Can become global mutable state. |
BehaviorSubject | Stores latest value and emits it to new subscribers. | Good for current state. |
ReplaySubject | Replays previous values. | Always bound buffer size/time if possible. |
share | Shares one subscription while subscribers exist. | Good for avoiding duplicated side effects. |
shareReplay | Shares subscription and replays cached values. | Use correct reset/refCount behavior to avoid stale cache. |
connectable | Creates a shared Observable whose connection can be started explicitly. | Use when connection timing must be controlled; prefer share for the common ref-counted case. |
map -> debounceTime -> distinctUntilChanged -> switchMap.paramMap -> map -> switchMap.exhaustMap.concatMap.combineLatest of data, filters, and selected state.takeUntilDestroyed from @angular/core/rxjs-interop; pass a DestroyRef when the call is outside an injection context.toSignal once in a stable field or service, provide an appropriate initial value, and avoid recreating it during template evaluation.import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
export class ProfileComponent {
private readonly destroyRef = inject(DestroyRef);
readonly profile$ = this.api.getProfile();
readonly profile = toSignal(this.profile$, { initialValue: null });
constructor() {
this.audit.events$.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(event => this.record(event));
}
}
mergeMap for search, causing older responses to overwrite newer results.switchMap for writes that must all complete.catchError at the end when only an inner request should recover.catchError.ReplaySubject and retaining too much memory.tap instead of keeping it explicit.map.filter.debounceTime.distinctUntilChanged.switchMap.mergeMap.concatMap.exhaustMap.combineLatest.forkJoin.withLatestFrom.tap.catchError.catchError return?of([]) for a fallback or throwError(() => new Error('Request failed')) to rethrow.finalize.retry.retry({ count, delay }), and retry only operations that are safe to repeat.startWith.scan.pairwise.bufferTime.share.shareReplay.fromEvent.interval.Explore 500+ free tutorials across 20+ languages and frameworks.