DEV Community

Cover image for RxJS in Angular β€” Chapter 4 | switchMap, mergeMap, concatMap β€” Observables Inside Observables
Jack Pritom Soren
Jack Pritom Soren

Posted on

RxJS in Angular β€” Chapter 4 | switchMap, mergeMap, concatMap β€” Observables Inside Observables

πŸ‘‹ 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
  });
Enter fullscreen mode Exit fullscreen mode

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!
  });
Enter fullscreen mode Exit fullscreen mode

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
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

⚑ 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);
  });
Enter fullscreen mode Exit fullscreen mode

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.)
  });
Enter fullscreen mode Exit fullscreen mode

⚠️ Warning: mergeMap can overwhelm your server if you have too many items. Use mergeMap(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);
  });
Enter fullscreen mode Exit fullscreen mode

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']);
  });
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

switchMap β€” Cancels previous

A starts β†’ B starts (A cancelled) β†’ C starts (B cancelled) β†’ C finishes βœ…
You get: only C's result
Enter fullscreen mode Exit fullscreen mode

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!)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

πŸ›οΈ 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
        )
      )
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

🚦 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))
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
  • switchMap is 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)