Use RxJS operators correctly to avoid memory leaks and subscription bugs
✓Works with OpenClaudeYou are the #1 Angular RxJS expert from Silicon Valley — the engineer that companies hire when their app has 50 memory leaks and components aren't unsubscribing properly. The user wants to use RxJS in Angular without leaks or bugs.
What to check first
- Audit existing subscriptions — every subscribe needs an unsubscribe
- Check for nested subscribes (almost always a bug — use higher-order operators)
- Identify hot vs cold observables
Steps
- Use async pipe in templates whenever possible — handles subscribe/unsubscribe automatically
- For .subscribe() calls in components, always unsubscribe in ngOnDestroy
- Use takeUntilDestroyed() (Angular 16+) for clean unsubscription
- Replace nested subscribes with switchMap, mergeMap, or concatMap
- Use shareReplay for observables that should be shared across subscribers
- Avoid subscribing inside template binding — use async pipe
Code
import { Component, inject, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Observable, of, switchMap, debounceTime, distinctUntilChanged, shareReplay } from 'rxjs';
import { HttpClient } from '@angular/common/http';
// BAD — manual subscription, leaks if you forget unsubscribe
@Component({...})
export class BadExample {
user: User | null = null;
private subscription: Subscription;
constructor(private http: HttpClient) {
this.subscription = this.http.get<User>('/api/me').subscribe(u => this.user = u);
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
// GOOD — async pipe handles everything
@Component({
template: `
@if (user$ | async; as user) {
<div>{{ user.name }}</div>
}
`,
})
export class GoodExample {
user$ = this.http.get<User>('/api/me');
constructor(private http: HttpClient) {}
}
// GOOD — takeUntilDestroyed (Angular 16+)
@Component({...})
export class ModernExample {
private http = inject(HttpClient);
private destroyRef = inject(DestroyRef);
user: User | null = null;
constructor() {
this.http.get<User>('/api/me')
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(u => this.user = u);
}
}
// BAD — nested subscribes (callback hell)
this.userService.getUser().subscribe(user => {
this.orderService.getOrders(user.id).subscribe(orders => {
this.displayOrders(orders);
});
});
// GOOD — switchMap chains observables
this.userService.getUser().pipe(
switchMap(user => this.orderService.getOrders(user.id)),
takeUntilDestroyed()
).subscribe(orders => this.displayOrders(orders));
// Operator choices for chaining:
// switchMap: cancel previous, take latest (search-as-you-type)
// concatMap: queue them, run sequentially
// mergeMap: run all in parallel
// exhaustMap: ignore new ones until current finishes (login button)
// Search-as-you-type with debounce + cancel
@Component({...})
export class SearchExample {
searchControl = new FormControl('');
results$ = this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(query => query ? this.api.search(query) : of([])),
shareReplay({ bufferSize: 1, refCount: true }),
);
constructor(private api: ApiService) {}
}
// Share an HTTP call across multiple subscribers
@Injectable({ providedIn: 'root' })
export class ConfigService {
config$ = this.http.get<Config>('/api/config').pipe(
shareReplay({ bufferSize: 1, refCount: false })
// refCount: false keeps the value cached even with no subscribers
);
constructor(private http: HttpClient) {}
}
// Avoid: subscribing inside template binding
@Component({
template: `
<!-- BAD: creates a new subscription every change detection -->
<div>{{ getUser() | async }}</div>
`,
})
export class Bad {
getUser(): Observable<User> {
return this.http.get('/api/me'); // re-subscribed constantly!
}
}
// GOOD: store as field, async pipe subscribes once
@Component({
template: `<div>{{ user$ | async }}</div>`,
})
export class Good {
user$ = this.http.get('/api/me');
}
Common Pitfalls
- Manually subscribing without unsubscribing — leaks every time the component re-creates
- Nested subscribes — race conditions and impossible to cancel cleanly
- Using mergeMap instead of switchMap for typeahead — old responses arrive after new ones
- Not sharing HTTP calls — same request fired N times for N subscribers
- Forgetting that Subjects must be completed in ngOnDestroy
When NOT to Use This Skill
- For Promise-based code that doesn't need streams — use async/await
- For one-shot HTTP calls — toSignal might be cleaner
How to Verify It Worked
- Use Angular DevTools to inspect subscription counts
- Add console.log to .subscribe callbacks to verify they don't fire after destroy
Production Considerations
- Use takeUntilDestroyed() everywhere in Angular 16+
- Migrate to signals where reactive updates don't need stream operators
- Audit subscriptions monthly with a custom RxJS interceptor
Related Angular Skills
Other Claude Code skills in the same category — free to download.
Angular Component
Create Angular components with inputs, outputs, and lifecycle hooks
Angular Service
Build Angular services with dependency injection and HTTP client
Angular Routing
Configure Angular routing with guards, resolvers, and lazy loading
Angular Forms
Build reactive forms with validation and custom validators
Angular RxJS
Use RxJS operators for async data flows in Angular
Angular NgRx
Set up NgRx state management with actions, reducers, and effects
Angular Testing
Write Angular unit tests with Jasmine and Karma
Angular Signals State Management
Use Angular Signals for reactive state without RxJS complexity
Want a Angular skill personalized to YOUR project?
This is a generic skill that works for everyone. Our AI can generate one tailored to your exact tech stack, naming conventions, folder structure, and coding patterns — with 3x more detail.