RxJS & Observables
Observables, subjects, operators (map, switchMap, combineLatest), async pipe
RxJS (Reactive Extensions for JavaScript) is a library for composing asynchronous and event-based programs using observable sequences. Angular uses RxJS throughout — HTTP client, Router events, ReactiveForms, and animations. Mastering the core operators and patterns is essential for Angular development.
Key Points
- Observable: lazy push-based stream of values — nothing happens until you subscribe()
- Subject: both observable and observer — can emit values manually; hot (multicast) vs cold (unicast) observables
- BehaviorSubject: Subject that holds a current value and emits it to new subscribers immediately
- map: transform each value; filter: keep values matching predicate; tap: side effects without changing the stream
- switchMap: cancel previous inner observable, start new one — ideal for HTTP calls triggered by input changes
- mergeMap: run inner observables concurrently — parallel requests
- concatMap: queue inner observables — sequential, ordered processing
- combineLatest: emit when any source emits, combining latest from all — dashboard data from multiple endpoints
- takeUntilDestroyed() (Angular 16+): auto-unsubscribe when component destroys — no more ngOnDestroy boilerplate
| Operator | Inner observables | Use case |
|---|---|---|
| switchMap | Cancel previous, start new | Search-as-you-type, navigation |
| mergeMap | All run concurrently | Parallel independent requests |
| concatMap | Queue, run one at a time | Sequential uploads, ordered jobs |
| exhaustMap | Ignore new until current completes | Prevent double-submit on button click |
RxJS in Angular: switchMap search, combineLatest dashboard, BehaviorSubject state service, takeUntilDestroyed
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({ standalone: true, imports: [AsyncPipe, ReactiveFormsModule], ... })
export class SearchComponent {
private destroyRef = inject(DestroyRef);
private search = inject(SearchService);
query = new FormControl('');
// Search-as-you-type — switchMap cancels previous request
results$ = this.query.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
filter(q => q!.length >= 2),
switchMap(q => this.search.find(q).pipe(
catchError(() => of([])) // don't kill stream on error
)),
takeUntilDestroyed(this.destroyRef) // auto-cleanup
);
}
// combineLatest — dashboard with multiple data sources
@Component({ template: '...' })
export class DashboardComponent {
vm$ = combineLatest({
users: inject(UserService).users$,
orders: inject(OrderService).recent$,
metrics: inject(MetricsService).live$
}).pipe(
map(({ users, orders, metrics }) => ({ users, orders, metrics }))
);
}
// BehaviorSubject for service state
@Injectable({ providedIn: 'root' })
export class CartService {
private items$ = new BehaviorSubject<CartItem[]>([]);
readonly cart$ = this.items$.asObservable();
readonly count$ = this.cart$.pipe(map(items => items.length));
addItem(item: CartItem) {
this.items$.next([...this.items$.value, item]);
}
}
// Template — async pipe handles subscribe/unsubscribe
// @if (vm$ | async; as vm) {
// <app-user-list [users]="vm.users"/>
// <app-orders [orders]="vm.orders"/>
// }Real-World Example
switchMap is the solution to a race condition that plagues naive HTTP request handling: the user types fast and requests return out of order. Without switchMap, the last response shown might be for an earlier query. With switchMap, each new keystroke cancels the previous HTTP request (via AbortController under the hood) — only the latest request's response is shown.