RxJS Best Practices in Angular
Writing maintainable and performant Angular applications with RxJS requires following certain best practices. Here are the most important ones.
1. Use the async Pipe Whenever Possible
Section titled “1. Use the async Pipe Whenever Possible”Avoid manual subscriptions in components. The async pipe handles subscription and unsubscription automatically.
Good:
<div>{{ data$ | async }}</div>Bad:
data: any;ngOnInit() { this.service.getData().subscribe(d => this.data = d); }ngOnDestroy() { this.sub?.unsubscribe(); }2. Unsubscribe Properly
Section titled “2. Unsubscribe Properly”If you must subscribe manually (e.g., in a service), use one of these patterns:
takeUntilwith a destroy subjecttake(1)for single‑emission Observables (like HTTP requests, though Angular HTTP completes automatically)first()etc.
3. Avoid Nesting Subscriptions
Section titled “3. Avoid Nesting Subscriptions”Nesting subscriptions leads to messy code and memory leaks. Use higher‑order mapping operators (switchMap, mergeMap, etc.) instead.
Bad:
this.route.params.subscribe((params) => { this.service.getData(params.id).subscribe((data) => { // handle data });});Good:
this.route.params.pipe(switchMap(params => this.service.getData(params.id))).subscribe(data => // handle data);4. Keep Streams Pure with pipe and Operators
Section titled “4. Keep Streams Pure with pipe and Operators”Always use operators inside pipe to transform data. Avoid having logic inside subscribe that could be done with operators.
5. Use catchError to Handle Errors Gracefully
Section titled “5. Use catchError to Handle Errors Gracefully”Always handle errors in your streams to prevent the entire stream from breaking.
this.http.get("/api/data").pipe( catchError((err) => { console.error("Error:", err); return of([]); // fallback }),);6. Leverage Subjects for Multicasting
Section titled “6. Leverage Subjects for Multicasting”When multiple parts of your app need to listen to the same data source, use a BehaviorSubject or ReplaySubject and expose it as an observable with .asObservable().
private userSubject = new BehaviorSubject<User | null>(null);user$ = this.userSubject.asObservable();
updateUser(user: User) {this.userSubject.next(user);}7. Prefer Declarative Approach over Imperative
Section titled “7. Prefer Declarative Approach over Imperative”Define Observables as properties that combine other Observables, rather than creating subscriptions in methods.
Declarative:
filteredTodos$ = combineLatest([this.todos$, this.filter$]).pipe(map(([todos, filter]) => todos.filter((t) => t.text.includes(filter))));Imperative (avoid):
filteredTodos: Todo[];ngOnInit() { this.todos$.subscribe(todos => { this.filter$.subscribe(filter => { this.filteredTodos = todos.filter(t => t.text.includes(filter)); }); });}8. Use Appropriate Schedulers for Tests
Section titled “8. Use Appropriate Schedulers for Tests”When writing tests, use TestScheduler to test asynchronous streams in a deterministic way (marble testing).
9. Name Observables with a Trailing $
Section titled “9. Name Observables with a Trailing $”This convention makes it clear that a variable is an Observable.
users$: Observable<User[]>;10. Clean Up Global Observables in Services
Section titled “10. Clean Up Global Observables in Services”If a service holds an Observable that never completes (like a Subject), ensure it is cleaned up when the service is destroyed (e.g., by using Angular’s @Injectable({providedIn: 'root'}) which is a singleton and not destroyed; but if you have a non‑root service, you can implement OnDestroy).
11. Use shareReplay for Caching
Section titled “11. Use shareReplay for Caching”When you have an expensive Observable that should be shared across subscribers (e.g., HTTP request result), use shareReplay to cache the last emitted value.
data$ = this.http.get("/api/data").pipe(shareReplay(1));12. Avoid Memory Leaks in Infinite Observables
Section titled “12. Avoid Memory Leaks in Infinite Observables”Always unsubscribe from infinite Observables like interval, fromEvent, or Subjects that never complete.
Following these practices will lead to cleaner, more maintainable, and bug‑free Angular applications. RxJS is powerful, but with great power comes great responsibility – always think about the lifecycle of your subscriptions.