| name | rxjs-operators |
| description | Essential RxJS operators and patterns for Angular development - transformation, filtering, combination, and error handling operators |
RxJS Operators for Angular
Purpose: Essential RxJS operators every Angular developer should master
Level: Intermediate to Advanced
Version: RxJS 7+
Core Concepts
Operators are functions that enable composing asynchronous operations with observables. They transform, filter, combine, and manage observable streams.
Key Principles:
- Operators are pure functions
- They don't modify the source observable
- They return a new observable
- They can be chained together
Category 1: Transformation Operators
map
Purpose: Transform each value emitted
import { of } from 'rxjs';
import { map } from 'rxjs/operators';
of(1, 2, 3).pipe(
map(x => x * 10)
).subscribe(console.log);
interface User { id: number; name: string; }
interface UserDisplay { id: number; displayName: string; }
this.users$.pipe(
map((users: User[]) => users.map(u => ({
id: u.id,
displayName: u.name.toUpperCase()
})))
);
switchMap
Purpose: Switch to a new observable, canceling previous
Use when: Making HTTP requests based on user input
import { switchMap } from 'rxjs/operators';
this.searchTerm$.pipe(
debounceTime(300),
switchMap(term => this.http.get(`/api/search?q=${term}`))
).subscribe(results => console.log(results));
this.userId$.pipe(
switchMap(id => this.http.get(`/api/users/${id}`))
).subscribe(user => this.user.set(user));
Why switchMap? Automatically cancels previous HTTP request if new search term arrives.
mergeMap (flatMap)
Purpose: Merge all inner observables
Use when: You want all requests to complete, not cancel previous
import { mergeMap } from 'rxjs/operators';
this.clicks$.pipe(
mergeMap(event => this.analytics.track(event))
).subscribe();
this.files$.pipe(
mergeMap(file => this.uploadFile(file))
).subscribe(result => console.log('Uploaded:', result));
concatMap
Purpose: Process observables in order, wait for each to complete
Use when: Order matters (e.g., sequential API calls)
import { concatMap } from 'rxjs/operators';
this.queue$.pipe(
concatMap(task => this.processTask(task))
).subscribe(result => console.log('Processed:', result));
this.users$.pipe(
concatMap(user => this.http.post('/api/users', user))
).subscribe();
exhaustMap
Purpose: Ignore new values while current is processing
Use when: Prevent duplicate submissions
import { exhaustMap } from 'rxjs/operators';
this.submitClick$.pipe(
exhaustMap(() => this.http.post('/api/form', this.formData))
).subscribe();
this.loginAttempt$.pipe(
exhaustMap(credentials => this.auth.login(credentials))
).subscribe();
Category 2: Filtering Operators
filter
Purpose: Emit only values that pass a condition
import { filter } from 'rxjs/operators';
of(1, 2, 3, 4, 5).pipe(
filter(x => x % 2 === 0)
).subscribe(console.log);
this.user$.pipe(
filter(user => user !== null)
).subscribe(user => console.log(user.name));
this.emailInput$.pipe(
filter(email => this.isValidEmail(email))
).subscribe(email => this.checkAvailability(email));
debounceTime
Purpose: Wait for silence before emitting
Use when: Search input, window resize
import { debounceTime } from 'rxjs/operators';
this.searchInput$.pipe(
debounceTime(300),
switchMap(term => this.search(term))
).subscribe(results => this.results.set(results));
fromEvent(window, 'resize').pipe(
debounceTime(200)
).subscribe(() => this.handleResize());
throttleTime
Purpose: Emit first value, then ignore for duration
Use when: Scroll events, rapid clicks
import { throttleTime } from 'rxjs/operators';
fromEvent(window, 'scroll').pipe(
throttleTime(100)
).subscribe(() => this.checkScrollPosition());
this.buttonClick$.pipe(
throttleTime(1000)
).subscribe(() => this.handleClick());
distinctUntilChanged
Purpose: Only emit when value changes
import { distinctUntilChanged } from 'rxjs/operators';
this.searchInput$.pipe(
distinctUntilChanged(),
switchMap(term => this.search(term))
).subscribe();
this.userId$.pipe(
distinctUntilChanged(),
switchMap(id => this.loadUser(id))
).subscribe();
take / takeUntil
Purpose: Take specific number or until condition
import { take, takeUntil } from 'rxjs/operators';
this.stream$.pipe(
take(5)
).subscribe();
private destroy$ = new Subject<void>();
this.data$.pipe(
takeUntil(this.destroy$)
).subscribe(data => console.log(data));
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
this.data$.pipe(
takeUntilDestroyed()
).subscribe(data => console.log(data));
Category 3: Combination Operators
combineLatest
Purpose: Emit when ANY source emits (after all emit at least once)
Use when: Combining multiple form fields, filters
import { combineLatest } from 'rxjs';
combineLatest([
this.user$,
this.settings$
]).pipe(
map(([user, settings]) => ({ user, settings }))
).subscribe(data => console.log(data));
combineLatest([
this.searchTerm$,
this.category$,
this.priceRange$
]).pipe(
map(([search, category, price]) => ({
search, category, price
})),
switchMap(filters => this.fetchProducts(filters))
).subscribe(products => this.products.set(products));
forkJoin
Purpose: Emit when ALL sources complete (like Promise.all)
Use when: Loading multiple independent resources
import { forkJoin } from 'rxjs';
forkJoin({
user: this.http.get('/api/user'),
config: this.http.get('/api/config'),
permissions: this.http.get('/api/permissions')
}).subscribe(({ user, config, permissions }) => {
this.initialize(user, config, permissions);
});
forkJoin([
this.http.get('/api/products'),
this.http.get('/api/categories'),
this.http.get('/api/brands')
]).subscribe(([products, categories, brands]) => {
});
merge
Purpose: Emit from any source as soon as it emits
Use when: Combining event streams
import { merge } from 'rxjs';
merge(
this.clicks$,
this.hovers$,
this.focuses$
).subscribe(event => this.trackEvent(event));
merge(
this.manualRefresh$,
this.autoRefresh$,
this.dataChanged$
).pipe(
switchMap(() => this.loadData())
).subscribe();
withLatestFrom
Purpose: Combine with latest value from other observables
Use when: Need secondary data with primary stream
import { withLatestFrom } from 'rxjs/operators';
this.submitButton$.pipe(
withLatestFrom(this.form$, this.user$),
map(([_, formData, user]) => ({ formData, user }))
).subscribe(({ formData, user }) => {
this.submit(formData, user);
});
this.searchTerm$.pipe(
withLatestFrom(this.filters$, this.sortOrder$),
switchMap(([term, filters, sort]) =>
this.search(term, filters, sort)
)
).subscribe();
Category 4: Error Handling
catchError
Purpose: Catch errors and return fallback observable
import { catchError } from 'rxjs/operators';
import { of, EMPTY } from 'rxjs';
this.http.get('/api/data').pipe(
catchError(error => {
console.error('Failed to load:', error);
return of([]);
})
).subscribe(data => console.log(data));
this.http.get('/api/data').pipe(
catchError(() => EMPTY)
).subscribe();
this.http.get('/api/data').pipe(
catchError(error => {
console.error(error);
return throwError(() => error);
})
).subscribe();
retry
Purpose: Retry failed observable
import { retry } from 'rxjs/operators';
this.http.get('/api/data').pipe(
retry(3),
catchError(error => {
console.error('Failed after 3 retries');
return of([]);
})
).subscribe();
import { retry } from 'rxjs/operators';
this.http.get('/api/data').pipe(
retry({
count: 3,
delay: 1000
})
).subscribe();
Category 5: Utility Operators
tap
Purpose: Perform side effects without modifying stream
import { tap } from 'rxjs/operators';
this.http.get('/api/data').pipe(
tap(data => console.log('Received:', data)),
map(data => data.items),
tap(items => console.log('Mapped:', items))
).subscribe();
this.searchTerm$.pipe(
tap(term => this.analytics.track('search', { term })),
switchMap(term => this.search(term))
).subscribe();
this.loadData().pipe(
tap(() => this.loading.set(true)),
finalize(() => this.loading.set(false))
).subscribe();
shareReplay
Purpose: Share observable and replay values to new subscribers
import { shareReplay } from 'rxjs/operators';
private config$ = this.http.get('/api/config').pipe(
shareReplay(1)
);
this.config$.subscribe(config => console.log('Sub 1:', config));
this.config$.subscribe(config => console.log('Sub 2:', config));
finalize
Purpose: Execute code when observable completes or errors
import { finalize } from 'rxjs/operators';
this.loadData().pipe(
tap(() => this.loading.set(true)),
finalize(() => this.loading.set(false))
).subscribe({
next: data => console.log(data),
error: err => console.error(err)
});
Common Patterns
Pattern 1: Search with Debounce
this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.http.get(`/api/search?q=${term}`)),
catchError(() => of([]))
).subscribe(results => this.results.set(results));
Pattern 2: Auto-Save
this.form.valueChanges.pipe(
debounceTime(2000),
distinctUntilChanged(),
tap(() => this.saving.set(true)),
switchMap(value => this.http.put('/api/save', value)),
finalize(() => this.saving.set(false))
).subscribe();
Pattern 3: Polling
import { interval, switchMap } from 'rxjs';
interval(5000).pipe(
switchMap(() => this.http.get('/api/status')),
catchError(() => of(null))
).subscribe(status => this.status.set(status));
Pattern 4: Type-ahead with Minimum Length
this.searchInput$.pipe(
debounceTime(300),
distinctUntilChanged(),
filter(term => term.length >= 3),
switchMap(term => this.search(term)),
catchError(() => of([]))
).subscribe(results => this.results.set(results));
Decision Tree
Need to transform values? → map
Need to switch to new observable? → switchMap
Need to wait for all to complete? → forkJoin
Need to combine latest values? → combineLatest
Need to filter values? → filter
Need to handle errors? → catchError
Need to retry? → retry
Need to share result? → shareReplay
Need to debounce? → debounceTime
Need to throttle? → throttleTime
Best Practices
- Always unsubscribe - Use
takeUntilDestroyed() or async pipe
- Prefer
async pipe over manual subscriptions
- Use
switchMap for dependent HTTP requests
- Use
forkJoin for parallel independent requests
- Add error handling with
catchError
- Cache with
shareReplay for expensive operations
- Debounce user input with
debounceTime
- Log with
tap for debugging
Common Mistakes
❌ Memory leaks:
this.data$.subscribe(data => console.log(data));
✅ Fixed:
this.data$.pipe(
takeUntilDestroyed()
).subscribe(data => console.log(data));
template: `{{ data$ | async }}`
❌ Nested subscriptions:
this.users$.subscribe(users => {
this.http.get('/api/settings').subscribe(settings => {
});
});
✅ Fixed:
this.users$.pipe(
switchMap(users => this.http.get('/api/settings').pipe(
map(settings => ({ users, settings }))
))
).subscribe(({ users, settings }) => {
});
Summary
Master these operators:
- Transformation:
map, switchMap, mergeMap
- Filtering:
filter, debounceTime, distinctUntilChanged
- Combination:
combineLatest, forkJoin, withLatestFrom
- Error Handling:
catchError, retry
- Utility:
tap, shareReplay, take, takeUntil
Key Takeaway: Choose the right operator for the job. switchMap for search, forkJoin for parallel loads, combineLatest for combining streams.