π Welcome to Chapter 4!
Buckle up β this is the chapter that trips up even experienced developers. But don't worry. By the end, it will make total sense.
The question today is: What do you do when you have an Observable that produces values⦠and each value needs to trigger ANOTHER Observable?
Sounds weird? Let's make it concrete.
π€ The Problem β Observable Inside Observable
Imagine this scenario:
You have a search box. Every time the user types, you want to search the API for results.
The user typing = Observable (stream of text values)
The API call = another Observable
So you have an Observable that needs to trigger another Observable. This is called a higher-order Observable β an Observable of Observables.
// What you WANT to do (but this DOESN'T work properly):
searchInput.valueChanges
.pipe(
map(searchTerm => this.http.get(`/api/search?q=${searchTerm}`))
// β Now you have an Observable of Observables! π±
// subscribe would give you Observable objects, not the actual data!
)
.subscribe(result => {
// result is an Observable, not the search results!
console.log(result); // Observable { ... } β NOT what we want
});
To fix this, we need flattening operators β switchMap, mergeMap, and concatMap.
They all do the same basic thing: subscribe to the inner Observable automatically and give you the final result.
But they behave very differently when multiple requests overlap.
π The Three Brothers β An Analogy
Imagine you're asking a waiter to bring you food from 3 different restaurants.
switchMap β "Actually, cancel that last order. I changed my mind, bring me this new one instead."
β Cancels the previous request when a new one comes in
mergeMap β "Yes, get all three orders! Bring them as they're ready, any order."
β Runs all requests simultaneously, results come in any order
concatMap β "Wait for the first order to arrive, THEN go get the second one, THEN the third."
β Runs requests one at a time, in order
π switchMap β Cancel the Old, Start the New
switchMap is the most commonly used flattening operator in Angular. Use it when you only care about the most recent request.
Perfect for: Search boxes, autocomplete, navigation triggers
import { switchMap } from 'rxjs/operators';
import { fromEvent } from 'rxjs';
// Every time the user types, CANCEL the old search, start a new one
searchInput.valueChanges
.pipe(
debounceTime(300), // Wait 300ms after typing stops
switchMap(searchTerm =>
// switchMap subscribes to the HTTP call automatically
// If a new value comes in BEFORE this finishes, it CANCELS this request
this.http.get<Product[]>(`/api/products?search=${searchTerm}`)
)
)
.subscribe(products => {
this.searchResults = products; // β
Actual products, not an Observable!
});
Why switchMap here?
User types "iph" β request starts. Then they type "ipho" β cancel the "iph" request and start "ipho". No outdated results! No race conditions!
Full Real-World Search Component
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Observable } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap, startWith } from 'rxjs/operators';
@Component({
selector: 'app-search',
template: `
<input [formControl]="searchControl" placeholder="Search products..." />
<div *ngIf="isLoading">Searching... π</div>
<div *ngFor="let product of results$ | async" class="result-item">
<strong>{{ product.name }}</strong>
<span>ΰ§³{{ product.price }}</span>
</div>
`
})
export class SearchComponent implements OnInit {
searchControl = new FormControl('');
results$!: Observable<Product[]>;
isLoading = false;
constructor(private productService: ProductService) {}
ngOnInit(): void {
this.results$ = this.searchControl.valueChanges.pipe(
startWith(''), // Start with empty string (show all)
debounceTime(400), // Wait 400ms after typing
distinctUntilChanged(), // Don't search if value didn't change
switchMap(term => {
this.isLoading = true;
return this.productService.search(term || '');
}),
tap(() => this.isLoading = false) // Turn off loading when results arrive
);
}
}
β‘ mergeMap (aka flatMap) β Run Everything Simultaneously
mergeMap starts every new Observable immediately without cancelling old ones. Results come in as they finish β order is not guaranteed.
Perfect for: Parallel uploads, multiple simultaneous requests
import { mergeMap, from } from 'rxjs';
// Upload multiple files AT THE SAME TIME
const files = [file1, file2, file3];
from(files) // from() converts an array to an Observable
.pipe(
mergeMap(file => this.uploadService.upload(file))
// All 3 uploads start at the same time!
// Results come in as each one finishes
)
.subscribe(uploadResult => {
console.log('Upload complete:', uploadResult.fileName);
});
Real Example β Load User Profiles in Parallel
// You have an array of user IDs, and want to load all profiles at once
const userIds = [1, 2, 3, 4, 5];
from(userIds)
.pipe(
mergeMap(id => this.http.get<User>(`/api/users/${id}`))
// All 5 HTTP calls fire simultaneously!
)
.subscribe(user => {
this.users.push(user);
// Users appear as they load β might be out of order (5 before 3, etc.)
});
β οΈ Warning:
mergeMapcan overwhelm your server if you have too many items. UsemergeMap(fn, 3)to limit concurrent requests to 3 at a time.
πΆ concatMap β One at a Time, In Order
concatMap waits for each Observable to complete before starting the next one. Results are always in order.
Perfect for: Sequential operations, guaranteed ordering
import { concatMap, from } from 'rxjs';
// Process steps one at a time, in order
const steps = ['step1', 'step2', 'step3'];
from(steps)
.pipe(
concatMap(step => this.processStep(step))
// step1 must complete before step2 starts
// step2 must complete before step3 starts
)
.subscribe(result => {
console.log('Step completed:', result);
});
Real Example β Onboarding Steps in Order
// After registration, run these steps IN ORDER:
// 1. Create user account
// 2. Send welcome email
// 3. Set up default preferences
this.registrationService.register(formData)
.pipe(
concatMap(user => this.emailService.sendWelcome(user.email)),
concatMap(() => this.prefService.setupDefaults())
)
.subscribe(() => {
this.router.navigate(['/dashboard']);
});
Here, the email is sent only after account creation succeeds. Preferences are set only after the email is sent. Perfect ordering! β
π Side-by-Side Comparison
Let's say you click a button 3 times quickly (A, B, C), and each click triggers an API call.
Click A starts β [Request A takes 3 seconds]
Click B starts β [Request B takes 1 second]
Click C starts β [Request C takes 2 seconds]
switchMap β Cancels previous
A starts β B starts (A cancelled) β C starts (B cancelled) β C finishes β
You get: only C's result
mergeMap β All run at once
A, B, C all run simultaneously
B finishes first β A finishes β C finishes
You get: B, A, C (out of order!)
concatMap β One at a time in order
A starts β A finishes β B starts β B finishes β C starts β C finishes
You get: A, B, C (in perfect order, but slowest)
ποΈ Complete Real-World Example β Product Detail Page with Related Items
This example uses switchMap to load a product, then mergeMap to load related items in parallel:
product-detail.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable, EMPTY } from 'rxjs';
import { switchMap, mergeMap, toArray, catchError } from 'rxjs/operators';
import { from } from 'rxjs';
@Component({
selector: 'app-product-detail',
template: `
<div *ngIf="product$ | async as product">
<h1>{{ product.name }}</h1>
<p>{{ product.description }}</p>
<p class="price">ΰ§³{{ product.price }}</p>
<h2>Related Products</h2>
<div *ngFor="let related of relatedProducts$ | async">
{{ related.name }} β ΰ§³{{ related.price }}
</div>
</div>
`
})
export class ProductDetailComponent implements OnInit {
product$!: Observable<Product>;
relatedProducts$!: Observable<Product[]>;
constructor(
private route: ActivatedRoute,
private productService: ProductService
) {}
ngOnInit(): void {
// switchMap: When the route param changes, cancel old request, load new product
this.product$ = this.route.params.pipe(
switchMap(params => {
const productId = params['id'];
return this.productService.getProduct(productId);
}),
catchError(err => {
console.error('Failed to load product', err);
return EMPTY; // Return empty Observable on error
})
);
// Load related products in parallel using mergeMap
this.relatedProducts$ = this.product$.pipe(
switchMap(product =>
// Get all related IDs and load them simultaneously
from(product.relatedIds).pipe(
mergeMap(id => this.productService.getProduct(id)),
toArray() // Collect all results into one array
)
)
);
}
}
π¦ Route-Based Loading with switchMap
One of the most common real-world uses of switchMap in Angular is loading data based on route parameters:
@Component({ ... })
export class UserProfileComponent implements OnInit {
user$!: Observable<User>;
posts$!: Observable<Post[]>;
constructor(
private route: ActivatedRoute,
private userService: UserService
) {}
ngOnInit(): void {
// Every time the URL changes (e.g., /users/1 β /users/2)
// switchMap cancels the old request and starts a new one
this.user$ = this.route.paramMap.pipe(
switchMap(params => {
const userId = params.get('id')!;
return this.userService.getUser(userId);
})
);
// Load user's posts whenever the user changes
this.posts$ = this.user$.pipe(
switchMap(user => this.userService.getPosts(user.id))
);
}
}
This is incredibly elegant! When the user navigates to /users/3, the old request for /users/2 gets automatically cancelled. No stale data. β¨
π§ Quick Decision Guide
Use switchMap when:
- Search box / autocomplete
- Loading data from route params
- You only care about the latest request
- Cancelling outdated requests is desirable
Use mergeMap when:
- Parallel uploads
- Loading multiple independent items simultaneously
- Order doesn't matter
- You want maximum speed
Use concatMap when:
- Steps must happen in order
- Sequential processing
- Like a queue β first in, first out
π§ Chapter 4 Summary β What You Learned
- Sometimes you need an Observable that triggers another Observable β called a higher-order Observable
-
switchMapβ cancels the previous inner Observable when a new one starts (best for search) -
mergeMapβ runs all inner Observables simultaneously (best for parallel tasks) -
concatMapβ runs inner Observables one at a time in order (best for sequential steps) -
switchMapis the most commonly used in Angular β especially for HTTP + route params - All three operators automatically subscribe to the inner Observable and give you the unwrapped result
π Coming Up in Chapter 5...
We've covered transformation operators. Now let's learn about Subject β the two-way radio of RxJS.
A Subject is an Observable that you can manually push values into. It's essential for sharing data between components and services!
See you in Chapter 5! π
π RxJS Deep Dive Newsletter Series | Chapter 4 of 10
Follow me on : Github Linkedin Threads Youtube Channel
Top comments (0)