一键导入
rxjs-operators
Essential RxJS operators and patterns for Angular development - transformation, filtering, combination, and error handling operators
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Essential RxJS operators and patterns for Angular development - transformation, filtering, combination, and error handling operators
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Proven architectural patterns for building scalable Angular applications in enterprise environments with large teams
Doguhan Uluca's Router-First Architecture - The 7 steps for designing scalable Angular applications by defining routes before components
Angular Signals - Modern reactive state management patterns with computed signals, effects, and interoperability with RxJS
Comprehensive guide to Angular bundle optimization, code splitting, tree shaking, and lazy loading strategies
Deep dive into Angular change detection optimization, OnPush strategy, Signals, and Zone.js patterns
Secure authentication and authorization patterns including JWT, OAuth2, guards, and role-based access control
| name | rxjs-operators |
| description | Essential RxJS operators and patterns for Angular development - transformation, filtering, combination, and error handling operators |
Purpose: Essential RxJS operators every Angular developer should master
Level: Intermediate to Advanced
Version: RxJS 7+
Operators are functions that enable composing asynchronous operations with observables. They transform, filter, combine, and manage observable streams.
Key Principles:
Purpose: Transform each value emitted
import { of } from 'rxjs';
import { map } from 'rxjs/operators';
// Example 1: Simple transformation
of(1, 2, 3).pipe(
map(x => x * 10)
).subscribe(console.log);
// Output: 10, 20, 30
// Example 2: Object transformation
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()
})))
);
Purpose: Switch to a new observable, canceling previous
Use when: Making HTTP requests based on user input
import { switchMap } from 'rxjs/operators';
// Search as user types
this.searchTerm$.pipe(
debounceTime(300),
switchMap(term => this.http.get(`/api/search?q=${term}`))
).subscribe(results => console.log(results));
// Load user details when ID changes
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.
Purpose: Merge all inner observables
Use when: You want all requests to complete, not cancel previous
import { mergeMap } from 'rxjs/operators';
// Send analytics for each click (don't cancel)
this.clicks$.pipe(
mergeMap(event => this.analytics.track(event))
).subscribe();
// Process multiple files in parallel
this.files$.pipe(
mergeMap(file => this.uploadFile(file))
).subscribe(result => console.log('Uploaded:', result));
Purpose: Process observables in order, wait for each to complete
Use when: Order matters (e.g., sequential API calls)
import { concatMap } from 'rxjs/operators';
// Process queue in order
this.queue$.pipe(
concatMap(task => this.processTask(task))
).subscribe(result => console.log('Processed:', result));
// Sequential API calls
this.users$.pipe(
concatMap(user => this.http.post('/api/users', user))
).subscribe();
Purpose: Ignore new values while current is processing
Use when: Prevent duplicate submissions
import { exhaustMap } from 'rxjs/operators';
// Prevent double-click on submit button
this.submitClick$.pipe(
exhaustMap(() => this.http.post('/api/form', this.formData))
).subscribe();
// Login button (ignore clicks while logging in)
this.loginAttempt$.pipe(
exhaustMap(credentials => this.auth.login(credentials))
).subscribe();
Purpose: Emit only values that pass a condition
import { filter } from 'rxjs/operators';
// Only even numbers
of(1, 2, 3, 4, 5).pipe(
filter(x => x % 2 === 0)
).subscribe(console.log);
// Output: 2, 4
// Only non-null users
this.user$.pipe(
filter(user => user !== null)
).subscribe(user => console.log(user.name));
// Only valid emails
this.emailInput$.pipe(
filter(email => this.isValidEmail(email))
).subscribe(email => this.checkAvailability(email));
Purpose: Wait for silence before emitting
Use when: Search input, window resize
import { debounceTime } from 'rxjs/operators';
// Wait 300ms after user stops typing
this.searchInput$.pipe(
debounceTime(300),
switchMap(term => this.search(term))
).subscribe(results => this.results.set(results));
// Window resize handler
fromEvent(window, 'resize').pipe(
debounceTime(200)
).subscribe(() => this.handleResize());
Purpose: Emit first value, then ignore for duration
Use when: Scroll events, rapid clicks
import { throttleTime } from 'rxjs/operators';
// Handle scroll at most once per 100ms
fromEvent(window, 'scroll').pipe(
throttleTime(100)
).subscribe(() => this.checkScrollPosition());
// Rate-limit button clicks
this.buttonClick$.pipe(
throttleTime(1000)
).subscribe(() => this.handleClick());
Purpose: Only emit when value changes
import { distinctUntilChanged } from 'rxjs/operators';
// Only emit when search term actually changes
this.searchInput$.pipe(
distinctUntilChanged(),
switchMap(term => this.search(term))
).subscribe();
// Only emit when user ID changes
this.userId$.pipe(
distinctUntilChanged(),
switchMap(id => this.loadUser(id))
).subscribe();
Purpose: Take specific number or until condition
import { take, takeUntil } from 'rxjs/operators';
// Take first 5 values
this.stream$.pipe(
take(5)
).subscribe();
// Take until component destroyed
private destroy$ = new Subject<void>();
this.data$.pipe(
takeUntil(this.destroy$)
).subscribe(data => console.log(data));
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
// Modern approach with takeUntilDestroyed
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
this.data$.pipe(
takeUntilDestroyed()
).subscribe(data => console.log(data));
Purpose: Emit when ANY source emits (after all emit at least once)
Use when: Combining multiple form fields, filters
import { combineLatest } from 'rxjs';
// Wait for both user and settings to load
combineLatest([
this.user$,
this.settings$
]).pipe(
map(([user, settings]) => ({ user, settings }))
).subscribe(data => console.log(data));
// Combine multiple filters
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));
Purpose: Emit when ALL sources complete (like Promise.all)
Use when: Loading multiple independent resources
import { forkJoin } from 'rxjs';
// Load multiple resources on init
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);
});
// Parallel API calls
forkJoin([
this.http.get('/api/products'),
this.http.get('/api/categories'),
this.http.get('/api/brands')
]).subscribe(([products, categories, brands]) => {
// All loaded
});
Purpose: Emit from any source as soon as it emits
Use when: Combining event streams
import { merge } from 'rxjs';
// Combine multiple event sources
merge(
this.clicks$,
this.hovers$,
this.focuses$
).subscribe(event => this.trackEvent(event));
// Combine refresh triggers
merge(
this.manualRefresh$,
this.autoRefresh$,
this.dataChanged$
).pipe(
switchMap(() => this.loadData())
).subscribe();
Purpose: Combine with latest value from other observables
Use when: Need secondary data with primary stream
import { withLatestFrom } from 'rxjs/operators';
// Submit form with latest user data
this.submitButton$.pipe(
withLatestFrom(this.form$, this.user$),
map(([_, formData, user]) => ({ formData, user }))
).subscribe(({ formData, user }) => {
this.submit(formData, user);
});
// Apply filter with latest settings
this.searchTerm$.pipe(
withLatestFrom(this.filters$, this.sortOrder$),
switchMap(([term, filters, sort]) =>
this.search(term, filters, sort)
)
).subscribe();
Purpose: Catch errors and return fallback observable
import { catchError } from 'rxjs/operators';
import { of, EMPTY } from 'rxjs';
// Return empty array on error
this.http.get('/api/data').pipe(
catchError(error => {
console.error('Failed to load:', error);
return of([]); // Fallback value
})
).subscribe(data => console.log(data));
// Return empty observable (complete immediately)
this.http.get('/api/data').pipe(
catchError(() => EMPTY)
).subscribe();
// Re-throw after logging
this.http.get('/api/data').pipe(
catchError(error => {
console.error(error);
return throwError(() => error);
})
).subscribe();
Purpose: Retry failed observable
import { retry } from 'rxjs/operators';
// Retry 3 times on failure
this.http.get('/api/data').pipe(
retry(3),
catchError(error => {
console.error('Failed after 3 retries');
return of([]);
})
).subscribe();
// Retry with delay (RxJS 7+)
import { retry } from 'rxjs/operators';
this.http.get('/api/data').pipe(
retry({
count: 3,
delay: 1000 // Wait 1s between retries
})
).subscribe();
Purpose: Perform side effects without modifying stream
import { tap } from 'rxjs/operators';
// Log for debugging
this.http.get('/api/data').pipe(
tap(data => console.log('Received:', data)),
map(data => data.items),
tap(items => console.log('Mapped:', items))
).subscribe();
// Track analytics
this.searchTerm$.pipe(
tap(term => this.analytics.track('search', { term })),
switchMap(term => this.search(term))
).subscribe();
// Update loading state
this.loadData().pipe(
tap(() => this.loading.set(true)),
finalize(() => this.loading.set(false))
).subscribe();
Purpose: Share observable and replay values to new subscribers
import { shareReplay } from 'rxjs/operators';
// Cache HTTP request
private config$ = this.http.get('/api/config').pipe(
shareReplay(1) // Cache last value
);
// Multiple subscribers get same value
this.config$.subscribe(config => console.log('Sub 1:', config));
this.config$.subscribe(config => console.log('Sub 2:', config));
// Only one HTTP request made!
Purpose: Execute code when observable completes or errors
import { finalize } from 'rxjs/operators';
// Always hide loading spinner
this.loadData().pipe(
tap(() => this.loading.set(true)),
finalize(() => this.loading.set(false))
).subscribe({
next: data => console.log(data),
error: err => console.error(err)
// loading.set(false) runs regardless
});
this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.http.get(`/api/search?q=${term}`)),
catchError(() => of([]))
).subscribe(results => this.results.set(results));
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();
import { interval, switchMap } from 'rxjs';
interval(5000).pipe(
switchMap(() => this.http.get('/api/status')),
catchError(() => of(null))
).subscribe(status => this.status.set(status));
this.searchInput$.pipe(
debounceTime(300),
distinctUntilChanged(),
filter(term => term.length >= 3),
switchMap(term => this.search(term)),
catchError(() => of([]))
).subscribe(results => this.results.set(results));
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
takeUntilDestroyed() or async pipeasync pipe over manual subscriptionsswitchMap for dependent HTTP requestsforkJoin for parallel independent requestscatchErrorshareReplay for expensive operationsdebounceTimetap for debugging❌ Memory leaks:
// Bad - no unsubscribe
this.data$.subscribe(data => console.log(data));
✅ Fixed:
// Good - auto unsubscribe
this.data$.pipe(
takeUntilDestroyed()
).subscribe(data => console.log(data));
// Or use async pipe
template: `{{ data$ | async }}`
❌ Nested subscriptions:
// Bad - pyramid of doom
this.users$.subscribe(users => {
this.http.get('/api/settings').subscribe(settings => {
// ...
});
});
✅ Fixed:
// Good - use switchMap
this.users$.pipe(
switchMap(users => this.http.get('/api/settings').pipe(
map(settings => ({ users, settings }))
))
).subscribe(({ users, settings }) => {
// ...
});
Master these operators:
map, switchMap, mergeMapfilter, debounceTime, distinctUntilChangedcombineLatest, forkJoin, withLatestFromcatchError, retrytap, shareReplay, take, takeUntilKey Takeaway: Choose the right operator for the job. switchMap for search, forkJoin for parallel loads, combineLatest for combining streams.