RxJS is not just a library — it’s the backbone of every reactive Angular application. Even with Signals in Angular 16+, RxJS remains the king for HTTP, real-time data, complex forms, typeahead search, and WebSockets.
In this article you will learn:
Core concepts explained practically
- The 4 flattening operators (switchMap, mergeMap, concatMap, exhaustMap) with clear comparison
- Full ready-to-copy examples (service + component + HTML)
- Advanced RxJS + WebSockets (real-time chat & live updates)
- Common mistakes that kill performance and cause memory leaks
- 2026 best practices (toSignal, takeUntilDestroyed, rxResource, etc.)
1. Quick Intro: Why RxJS in Angular?
- Angular is built on Observables, not Promises:
- HttpClient returns Observable
- Form.valueChanges returns Observable
- Router events, WebSockets, etc.
Benefits:
- Automatic cancellation
- Powerful operators
- Centralized error handling
- Seamless integration with Signals (toSignal / toObservable )
2. Core Concepts (Fast & Clear)
- Observable – Stream of data (cold/hot)
- Observer – Listens with next, error, complete
- Operators – pipe() magic (map, filter, tap, etc.)
- Subject – Observable + Observer (BehaviorSubject is most used)
- Subscription – Must be cleaned (but in 2026 we use takeUntilDestroyed)
3. The 4 Flattening Operators – MUST KNOW (2026 Edition)
These operators decide what happens when a new inner Observable arrives while the previous one is still running.
switchMap
Behavior: Cancels the previous request and starts a new one
Use Case:
- Typeahead search
- Autocomplete
- Scenarios where only the latest value matters
- Cancels previous? Yes
- Order preserved? No
- Performance: Best for this type of scenario
switchMap Example (Most Used – Typeahead)
search$ = this.query$.pipe(
debounceTime(300),
switchMap(term => this.userService.searchUsers(term)) // ← cancels old HTTP
);
mergeMap
Behavior: Runs all requests in parallel
Use Case:
- Independent API calls
- When order doesn’t matter
- Cancels previous? No
- Order preserved? No
- Performance: Good
mergeMap Example (Parallel Requests) TypeScript
getUserAndPosts(userId: number) {
return this.http.get(`/users/${userId}`).pipe(
mergeMap(user =>
this.http.get(`/posts?userId=${userId}`).pipe(
map(posts => ({ ...user, posts }))
)
)
);
}
concatMap
Behavior: Waits for the previous request to complete before starting the next one
Use Case:
Sequential flows (e.g., login → fetch profile)
Steps that must happen in order
Cancels previous? No
Order preserved? Yes
Performance: Safe but slower
concatMap Example (Sequential)TypeScript
saveAndRefresh() {
return this.saveForm().pipe(
concatMap(() => this.refreshData()) // waits for save to finish
);
}
exhaustMap
Behavior: Ignores new requests while the current one is still running
Use Case:
- Form submissions
- Preventing double clicks on buttons
- Cancels previous? No (it ignores new ones)
- Order preserved? Yes
- Performance: Excellent for forms
exhaustMap Example (Prevent Double Submit) TypeScript
submitButtonClicked$ = new Subject<void>();
submit$ = this.submitButtonClicked$.pipe(
exhaustMap(() => this.api.saveData()) // ignores clicks while saving
);
Rule of thumb in 2026:
90% of the time → switchMap
Parallel → mergeMap
Order matters → concatMap
User actions → exhaustMap
4. Example 1: HTTP + switchMap + toSignal (Modern Angular 2026)
user.service.ts
@Injectable({ providedIn: 'root' })
export class UserService {
searchUsers(term: string): Observable<User[]> {
if (!term) return of([]);
return this.http.get<User[]>(`https://jsonplaceholder.typicode.com/users?q=${term}`);
}
}
search.component.ts (Standalone) TypeScript
@Component({
selector: 'app-search',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<input [(ngModel)]="query" placeholder="Search users...">
<div *ngIf="loading()">Loading...</div>
<ul>
<li *ngFor="let user of users()">{{ user.name }}</li>
</ul>
`
})
export class SearchComponent {
query = signal('');
loading = signal(false);
users = toSignal(
toObservable(this.query).pipe(
debounceTime(300),
tap(() => this.loading.set(true)),
switchMap(term => this.userService.searchUsers(term)),
tap(() => this.loading.set(false)),
takeUntilDestroyed()
),
{ initialValue: [] }
);
constructor(private userService: UserService) {}
}
5. Example 2: Reactive Forms + combineLatest + toSignal TypeScript
fullName = toSignal(
combineLatest([
this.form.get('firstName')!.valueChanges,
this.form.get('lastName')!.valueChanges
]).pipe(map(([f, l]) => `${f} ${l}`)),
{ initialValue: '' }
);
6. Advanced Example: RxJS + WebSockets (Real-time Chat 2026)
websocket.service.ts (Modern & Clean)
import { WebSocketSubject } from 'rxjs/webSocket';
import { Injectable } from '@angular/core';
import { shareReplay, filter, takeUntilDestroyed } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class WebSocketService {
private socket$ = new WebSocketSubject<Message>('wss://your-chat-api.com');
public messages$ = this.socket$.pipe(
filter(msg => msg.type === 'chat'),
shareReplay(1) // cache latest message
);
sendMessage(text: string) {
this.socket$.next({ type: 'chat', text });
}
close() {
this.socket$.complete();
}
}
chat.component.ts (Real-time with Signals) TypeScript
@Component({
standalone: true,
template: `
<div *ngFor="let msg of messages()">{{ msg.text }}</div>
<input #input>
<button (click)="send(input.value); input.value=''">Send</button>
`
})
export class ChatComponent {
messages = toSignal(this.ws.messages$, { initialValue: [] });
constructor(private ws: WebSocketService) {
// Auto cleanup
this.ws.messages$.pipe(takeUntilDestroyed()).subscribe();
}
send(text: string) {
if (text) this.ws.sendMessage(text);
}
}
Pro tip: Use shareReplay(1) + toSignal for instant UI updates without flicker.
7. Common RxJS Mistakes (and How to Fix Them in 2026)
- Forgetting to unsubscribe → Memory leaks
Fix: Always use takeUntilDestroyed() (Angular 16+)
- Using mergeMap instead of switchMap for search → 50 requests fired
Fix: switchMap = cancel previous
- Nested subscribe() (subscribe hell)
Fix: Use switchMap, concatMap, combineLatest
- Not handling errors → App crashes Fix:
catchError(err => {
this.toastr.error('Something went wrong');
return of(null);
})
- Using async pipe + manual subscribe in same component
Fix: Choose one (prefer toSignal in 2026)
- Cold Observable called multiple times
Fix: Use shareReplay(1) or share() in services
- Ignoring exhaustMap on buttons → Double payments
Fix: exhaustMap on submit actions
8. 2026 Best Practices (Apply These Today)
- Use takeUntilDestroyed() everywhere
- Convert to toSignal() for templates (faster than async pipe)
- Use toObservable() when you need RxJS pipeline from a signal
- Centralize error handling with catchError + global interceptor
- shareReplay(1) for cached services
- New rxResource (Angular 19+) for data fetching with signals
- Never subscribe in services — return Observable always
Final Words
RxJS + Signals in Angular 2026 is the most powerful combination ever.
Master switchMap (90% of cases), use WebSocketSubject for real-time, and never forget takeUntilDestroyed.
Top comments (0)