In Angular, most async APIs expose RxJS Observables (HttpClient, form valueChanges, router events). An Observable is a lazy stream that can emit 0..N values over time and supports cancellation via unsubscribe. Interview focus: cold vs hot streams, subscription/teardown, avoiding memory leaks, and composing streams with operators instead of nested subscribes.
Observables in Angular: what they are, why RxJS matters, and how to use them correctly (async pipe, cancellation, operators)
Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.
Core idea
Observable (RxJS) = a lazy stream of values over time. Nothing happens until you subscribe(). It can emit next values, then either complete or error. Unsubscribe triggers teardown (cancels ongoing work like HTTP requests). Angular uses RxJS Observables as the default async primitive.
Concept | What it means | Why interviewers care |
|---|---|---|
Lazy | No execution until subscribed. | Explains why pipelines do nothing unless subscribed/async-piped. |
0..N values | Can emit many values over time (unlike Promise). | Fits UI/event streams (input, websockets, router events). |
Teardown (unsubscribe) | Subscription can be disposed; producer stops work. | Memory leak + cancel-in-flight HTTP are real-world issues. |
Operators | Use | Senior signal: composition over nested subscribes. |
Angular ↔ RxJS relationship
RxJS provides the Observable implementation and operators. Angular exposes Observables in core APIs (most notably HttpClient, reactive forms valueChanges/statusChanges, and Router.events).
Angular API | What you get | Typical pattern |
|---|---|---|
HttpClient | Cold Observable (per subscription), usually completes after one response. | Compose with operators + consume via |
Reactive Forms |
| Debounce + distinct + switchMap (typeahead/search). |
Router |
| Filter to |
Cold vs hot (senior interview hotspot)
Cold = each subscriber triggers a new execution (e.g., HTTP request per subscribe). Hot = source exists independently and subscribers tap into it (e.g., DOM events, Subjects). If you want to share one execution across many subscribers, you must multicast (e.g., shareReplay).
Type | Example | Behavior |
|---|---|---|
Cold |
| Each subscribe can re-run the request (unless shared). |
Hot |
| Emits regardless of who listens; subscribers see emissions while subscribed. |
Shared |
| One execution, cached value(s) shared to multiple subscribers. |
Best practice in Angular components
Prefer async pipe over manual subscriptions. It subscribes/unsubscribes automatically with the view lifecycle, which prevents leaks and works well with OnPush.
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { AsyncPipe, NgFor } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { catchError, map, of, shareReplay } from 'rxjs';
type User = { id: number; name: string };
@Component({
selector: 'app-users',
standalone: true,
imports: [NgFor, AsyncPipe],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<ul>
<li *ngFor="let u of (users$ | async)">{{ u.name }}</li>
</ul>
`
})
export class UsersComponent {
private http = inject(HttpClient);
// Cold source (HTTP) -> shared + cached for this component instance
readonly users$ = this.http.get<User[]>('/api/users').pipe(
catchError(() => of([] as User[])),
shareReplay({ bufferSize: 1, refCount: true })
);
}
Operator choice: switchMap vs mergeMap vs concatMap vs exhaustMap
When you map to an Observable (HTTP, dialogs, etc.), you get a “higher-order” stream. Picking the right flattening operator is a common interview probe.
Operator | Behavior | When to use |
|---|---|---|
| Cancels previous inner stream when a new value arrives. | Typeahead/search, route changes, latest-only UI. |
| Runs inners concurrently. | Fire-and-forget or parallel requests (with care). |
| Queues inners, runs one at a time in order. | Ordered writes (save steps), rate-limited sequences. |
| Ignores new values while an inner is running. | Prevent double-submit, login button, “ignore spam clicks”. |
Example: typeahead done correctly
Debounce user input, avoid duplicate queries, cancel in-flight requests, and keep errors from killing the stream.
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { AsyncPipe, NgFor } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { catchError, debounceTime, distinctUntilChanged, filter, of, startWith, switchMap } from 'rxjs';
type User = { id: number; name: string };
@Component({
selector: 'app-user-search',
standalone: true,
imports: [ReactiveFormsModule, NgFor, AsyncPipe],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<input [formControl]="q" placeholder="Search users" />
<ul>
<li *ngFor="let u of (results$ | async)">{{ u.name }}</li>
</ul>
`
})
export class UserSearchComponent {
private http = inject(HttpClient);
readonly q = new FormControl('', { nonNullable: true });
readonly results$ = this.q.valueChanges.pipe(
startWith(this.q.value),
debounceTime(250),
mapTrim,
filter(v => v.length >= 2),
distinctUntilChanged(),
switchMap(v =>
this.http.get<User[]>(`/api/users?q=${encodeURIComponent(v)}`).pipe(
catchError(() => of([] as User[]))
)
)
);
}
function mapTrim(source$: any) {
return source$.pipe((v: string) => v.trim());
}
Subscription management (avoid leaks)
If you must manually subscribe (imperative side effects), ensure teardown. In modern Angular, takeUntilDestroyed() is the clean default; otherwise unsubscribe in ngOnDestroy.
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { fromEvent, map } from 'rxjs';
@Component({
selector: 'app-resize-log',
standalone: true,
template: `Resize listener active (check console)`
})
export class ResizeLogComponent {
private destroyRef = inject(DestroyRef);
constructor() {
fromEvent(window, 'resize').pipe(
map(() => window.innerWidth),
takeUntilDestroyed(this.destroyRef)
).subscribe(w => console.log('width', w));
}
}
Common mistake | Why it’s bad | Fix |
|---|---|---|
Nested subscribes | Hard to cancel, hard to handle errors, messy control flow. | Use operators ( |
Manual subscribe in components for display data | Leak risk + extra state + OnPush pain. | Expose |
Using | Can keep values alive longer than intended; refCount matters. | Use |
Letting errors terminate long-lived streams | One error can kill | Handle with |
Observable vs Promise | Observable | Promise |
|---|---|---|
Values | 0..N over time | Single resolution |
Execution | Lazy (starts on subscribe) | Eager (starts immediately) |
Cancellation | Yes (unsubscribe triggers teardown) | No built-in cancellation |
Composition | Operators + stream algebra | then/catch/finally (less expressive for streams) |
Observables are RxJS streams used throughout Angular. Key points: they’re lazy, can emit multiple values, and support teardown via unsubscribe. In Angular, prefer composition with operators + async pipe (or takeUntilDestroyed for imperative subscriptions). Know cold vs hot and pick the right flattening operator (switchMap/mergeMap/concatMap/exhaustMap) to control cancellation and concurrency.