| name | rxjs |
| description | RxJS patterns for Angular |
| allowed-tools | [] |
Ben Lesh: Reactive Patterns
Ben Lesh's core belief: Observables are for events over time, not single values. Use the right tool: Promises for single async values, Observables for streams.
The Foundational Principle
"RxJS is a library for composing asynchronous and event-based programs using observable sequences."
The key word is sequences. If you have one value, you probably don't need RxJS.
Core Principles
1. Know When NOT to Use RxJS
RxJS adds complexity. Use it when you have:
- Multiple values over time (events, WebSockets, polling)
- Need to combine multiple async sources
- Need cancellation
- Need complex transformation of async data
Don't need RxJS:
async getUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
Need RxJS:
searchResults$ = this.searchTerm$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.searchService.search(term))
);
2. Prefer Higher-Order Mapping Operators
switchMap, mergeMap, concatMap, exhaustMap - know the difference.
| Operator | Behavior | Use When |
|---|
switchMap | Cancels previous | Type-ahead search |
mergeMap | Runs all in parallel | Independent requests |
concatMap | Queues, runs in order | Order matters |
exhaustMap | Ignores new until done | Prevent double-submit |
Not this:
this.searchTerm$.subscribe(term => {
this.searchService.search(term).subscribe(results => {
this.results = results;
});
});
This:
this.results$ = this.searchTerm$.pipe(
debounceTime(300),
switchMap(term => this.searchService.search(term))
);
3. Avoid Nested Subscribes
If you're subscribing inside a subscribe, you're doing it wrong.
Not this:
this.user$.subscribe(user => {
this.ordersService.getOrders(user.id).subscribe(orders => {
this.orders = orders;
});
});
This:
this.orders$ = this.user$.pipe(
switchMap(user => this.ordersService.getOrders(user.id))
);
4. Use Subjects Sparingly
Subjects are escape hatches. Prefer declarative streams.
Not this:
class UserService {
private usersSubject = new BehaviorSubject<User[]>([]);
users$ = this.usersSubject.asObservable();
loadUsers() {
this.http.get<User[]>('/users').subscribe(users => {
this.usersSubject.next(users);
});
}
}
This:
class UserService {
private refresh$ = new Subject<void>();
users$ = this.refresh$.pipe(
startWith(undefined),
switchMap(() => this.http.get<User[]>('/users')),
shareReplay(1)
);
refresh() {
this.refresh$.next();
}
}
5. Share Subscriptions with shareReplay
Multiple subscribers shouldn't trigger multiple HTTP calls.
Not this:
user$ = this.http.get<User>('/api/user');
<div>{{ (user$ | async)?.name }}</div>
<div>{{ (user$ | async)?.email }}</div>
This:
user$ = this.http.get<User>('/api/user').pipe(
shareReplay(1)
);
<div>{{ (user$ | async)?.name }}</div>
<div>{{ (user$ | async)?.email }}</div>
shareReplay options:
shareReplay({ bufferSize: 1, refCount: true })
6. Handle Errors Properly
Errors terminate streams. Catch and recover.
Not this:
results$ = searchTerm$.pipe(
switchMap(term => this.searchService.search(term))
);
This:
results$ = searchTerm$.pipe(
switchMap(term => this.searchService.search(term).pipe(
catchError(error => {
console.error('Search failed:', error);
return of([]);
})
))
);
7. Unsubscribe Properly
Memory leaks from forgotten subscriptions are the #1 RxJS bug.
Options (best to worst):
@Component({
template: `<div *ngFor="let item of items$ | async">{{ item }}</div>`
})
@Component({})
class MyComponent {
items$ = this.service.getItems().pipe(
takeUntilDestroyed()
);
}
@Component({})
class MyComponent implements OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit() {
this.service.getItems().pipe(
takeUntil(this.destroy$)
).subscribe(items => this.items = items);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
8. Use Signals for Synchronous State (Angular 16+)
Signals are simpler than BehaviorSubject for synchronous state.
Before (RxJS for everything):
class CounterService {
private countSubject = new BehaviorSubject(0);
count$ = this.countSubject.asObservable();
increment() {
this.countSubject.next(this.countSubject.value + 1);
}
}
After (Signals for sync state):
class CounterService {
count = signal(0);
doubleCount = computed(() => this.count() * 2);
increment() {
this.count.update(c => c + 1);
}
}
Rule: Use Signals for synchronous state, Observables for async streams.
9. Combine Streams Declaratively
Use combination operators, not imperative code.
vm$ = combineLatest([
this.user$,
this.permissions$,
this.settings$
]).pipe(
map(([user, permissions, settings]) => ({ user, permissions, settings }))
);
allData$ = forkJoin([
this.usersService.getAll(),
this.rolesService.getAll()
]);
result$ = race([
this.cache.get(key),
this.api.get(key)
]);
10. Debug with tap, Not console.log Everywhere
results$ = searchTerm$.pipe(
tap(term => console.log('Search term:', term)),
switchMap(term => this.searchService.search(term)),
tap(results => console.log('Results:', results.length))
);
The Lesh Test
Before using RxJS, ask:
- Do I have multiple values over time? If no, consider Promise/Signal
- Am I nesting subscribes? Flatten with higher-order operators
- Am I using Subject as a crutch? Can I make it declarative?
- Will every subscriber trigger side effects? Use shareReplay
- Have I handled errors? Streams terminate on error
- Will this unsubscribe? Use async pipe or takeUntilDestroyed
When Reviewing Code
Apply these checks:
Common Patterns
search$ = this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
filter(term => term.length >= 2),
switchMap(term => this.searchService.search(term).pipe(
catchError(() => of([]))
))
);
data$ = timer(0, 30000).pipe(
switchMap(() => this.api.getData()),
retry(3),
shareReplay(1)
);
save(item: Item) {
const optimistic$ = of(item);
const server$ = this.api.save(item).pipe(delay(0));
(optimistic$, server$);
}
When NOT to Use This Skill
Use a different skill when:
- Designing component architecture → Use
angular-core
- Optimizing bundle size → Use
angular-perf
- Single async values → Use Promises
- Synchronous state → Use Signals
Ben Lesh is the RxJS/reactive skill—use it for streams, events, and complex async composition.
Sources
- Lesh, RxJS documentation (as lead maintainer)
- "RxJS in Action" - Daniels & Atencio
- Lesh's conference talks and blog posts
- Angular documentation - RxJS integration
"RxJS is powerful, but with great power comes great responsibility. Don't use it for everything." — Ben Lesh