My Logo

PUBLISHED OCTOBER 12, 2024

Teghen Donald Ticha
Lastly Updated: 36 minutes ago
Reading time 12 mins

switchMap vs mergeMap vs concatMap in RxJS

Explore the differences between RxJS switchMap, mergeMap, and concatMap for handling async operations with some practical browser-based examples and use cases.
switchMap vs mergeMap vs concatMap in RxJS

Prerequisite

Before diving into this article, It;s important that you have a basic understanding of the following:

  1. 1.JavaScript Promises and Async/Await
    Familiarity with how JavaScript handles asynchronous code using promises and async/await is essential for understanding RxJS and its operators.
  2. 2.Introductory Knowledge of RxJS
    A basic understanding of RxJS and the concept of observables will help you grasp how switchMap, mergeMap, and concatMap work.
    If you're new to RxJS, check out our .
  3. 3.JavaScript ES6+ Features
    Features like arrow functions, destructuring, and template literals are frequently used in RxJS code.
  4. 4.Client-Side JavaScript
    Since this article focuses on using RxJS in the browser, basic knowledge of DOM manipulation and event handling is beneficial.

RxJS(`Reactive Extensions for JavaScript`), is a powerful library designed for managing asynchronous data streams.

It provides a robust framework for composing and handling data that flows over time, enabling us to manage events, messages, and data in a more declarative and functional manner.

One of the core features of RxJS is its `combinatory operators`, which play a crucial role in managing asynchronous streams.


In modern web development, user interactions, HTTP requests, and various asynchronous operations are commonplace.

As applications grow in complexity, the need for efficient handling of these async operations becomes paramount.


Combinatory operators such as `switchMap`, `mergeMap`, and `concatMap` allow us to transform and manage these streams effectively, providing the tools to handle multiple sources of data and events seamlessly.

In this short guide we'll explore combinatory operators and discuss how leveraging them, helps us can create more responsive, maintainable, and scalable applications, ultimately improving the user experience and the efficiency.



What Are Higher-Order Observables?

Higher-order observables are observables that emit other observables.

Instead of emitting plain values, these observables emit other observable streams, allowing us to create complex asynchronous flows.

Managing higher-order observables directly can be challenging because you need to subscribe to inner observables to access their values.


Consider this example:

<!DOCTYPE html> 
<html lang="en-us">
    <head>
        <meta charset="utf-8">
        <script src="https://unpkg.com/rxjs@^7/dist/bundles/rxjs.umd.min.js"></script>
        <title>Play Book</title> 
        <style type="text/css">
            html, body {
                margin: 0;
                padding: 0;
                height: 100%;
                display: flex;
                flex-direction: column;
                justify-content: center;
            }
           button{
                background: purple;
                color: white;
                border: none;
                border-radius: 4px;
                padding: 10px 12px;
                cursor: pointer;
           }
        </style>
    </head>
    <body>       
        <button id="btn">Click me</button>
        <script>
            const {fromEvent, map, interval, take} = rxjs;

            const button = document.getElementById('btn');

            // Higher-order observable that emits a new observable( via the "of" operator) on each button click
            const higherOrder$ = fromEvent(button, 'click').pipe(
                map(() => interval(1000).pipe(take(3)))
            );

            // Subscribe to the higher-order observable and log values from inner observables
            higherOrder$.subscribe(innerObservable => {
                innerObservable.subscribe(value => {
                    console.log(value);
                });
            });
        </script>
    </body>
</html>

In this example, each time the button is clicked, a new observable is created that emits values (0,1,2) with a 1 second delay.

This produces non-deterministic results as clicking multiple times will produce out of order and difficult to tract results.

Also managing nested subscriptions directly can become complex and error-prone without higher-order mapping operators.

Higher-order Mapping Operators

Higher-order mapping operators are used to work with observables that emit other observables.

They enable us to manipulate the output of one observable based on the emissions from another observable.

Each of these operators serves a distinct purpose in transforming streams of events or asynchronous data.

  • switchMap: Transforms the items emitted by an observable into observables, subscribing to the latest one and cancelling previous subscriptions.
    It’s ideal for scenarios where you want to get the most recent result, such as in search functionalities.
  • mergeMap: Projects each emitted value from the source observable into a new observable and merges all the resulting observables into one.
    This operator allows for concurrent processing of emitted items, making it useful for handling multiple simultaneous requests.
  • concatMap: Similar to mergeMap, but it processes each observable sequentially.
    It ensures that each observable completes before moving to the next, making it perfect for scenarios where the order of operations matters.


Now that we have a foundational understanding of higher-order observables and their mapping operators, let’s dive deeper into mergeMap.

We will explore how it can be effectively utilized to handle concurrent API calls and enhance user interactions in your applications.

1. mergeMap - Handling Concurrent API Calls

mergeMap is a higher-order mapping operator that allows for the simultaneous execution of multiple inner observables.

When you use mergeMap, each time the source observable emits a value, mergeMap subscribes to the new inner observable while maintaining subscriptions to all previous inner observables.

This is particularly useful for operations where multiple asynchronous tasks can occur independently and do not rely on the results of one another.

Use Case

In modern web applications, it's common to handle multiple API calls triggered by user actions, such as fetching data from different endpoints in response to user interactions.

mergeMap enables us to initiate these requests in parallel without waiting for one to complete before starting another.


Let's look at an example.

Imagine a scenario where a user can click multiple buttons to fetch data from different endpoints simultaneously.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>RxJS Example: mergeMap with Buttons</title>
    <script src="https://unpkg.com/rxjs@^7/dist/bundles/rxjs.umd.min.js"></script>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
            text-align: center;
        }
        .btn {
            display: inline-block;
            padding: 10px 15px;
            margin: 25px;
            background-color: purple;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            transition: all .5s;
        }
        .btn:hover {
            transform: scale(1.2, 1.1);
        }
    </style>
</head>
<body>
    <h1>mergeMap Example</h1>
    <button class="btn" id="posts">Fetch Posts</button>
    <button class="btn" id="comments">Fetch Comments</button>
    <button class="btn" id="users">Fetch Users</button>

    <script>
        const { fromEvent, mergeMap, delay, map } = rxjs;
        const { ajax } = rxjs.ajax;

        const buttons = document.querySelectorAll('.btn');

        const buttonClicks$ = fromEvent(buttons, 'click').pipe(
            mergeMap(event => {
                const endpoint = event.target.getAttribute('id');
                return ajax.getJSON(`https://jsonplaceholder.typicode.com/${endpoint}`).pipe(
                    delay(Math.random() * 2500),
                    map(res => {
                        return {
                            type: endpoint,  
                            result: res
                        };
                    })
                );
            })
        );

        buttonClicks$.subscribe({
            next: data => console.log('from: ', data.type, 'result:', data.result),
            error: err => console.error('Error fetching data:', err)
        });
    </script>
</body>
</html>

Breakdown: Try clicking on each button in a defined sequence as fast as you can, look at the order in the console.

Repeat this step respecting the previous sequence and what you will notice is that, the order of clicks isn't preserved.

So this operator focuses exclusive of optimizing concurrent async operations.

  • The random delays simulate real-world scenarios where API responses can vary in time, demonstrating how mergeMap can handle multiple simultaneous requests effectively.
  • We have multiple buttons, each with a id attribute representing different API endpoints.
  • The fromEvent function listens for click events on the buttons.
    When a button is clicked, mergeMap takes the event and fetches data from the specified endpoint.

This approach enhances user experience by enabling quick responses to interactions, especially when user is likely to click multiple buttons in quick succession.



2. concatMap - Preserving Order in Async Tasks

concatMap is an RxJS operator that allows us to handle asynchronous operations while ensuring that the order of events is preserved.

It processes each observable one at a time, waiting for the current observable to complete before moving on to the next.

This is particularly useful in scenarios where the order of execution is crucial, such as processing sequential form submissions or uploads.


Use Case

In a scenario where a user has a list of buttons, each corresponding to fetching JSONPlaceholder user data by user ID, concatMap can be used to ensure that even if a button is clicked multiple times, the requests will be handled one after the other.

This guarantees that the responses are processed in the order the buttons were clicked, regardless of how long each API call takes.


Example

Here’s an implementation that demonstrates how concatMap can be used to fetch user data while preserving the order of button clicks:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>RxJS Example: concatMap with Buttons</title>
    <script src="https://unpkg.com/rxjs@^7/dist/bundles/rxjs.umd.min.js"></script>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
            text-align: center;
        }
        .btn {
            display: inline-block;
            padding: 10px 15px;
            margin: 25px;
            background-color: green;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            transition: all .5s;
        }
        .btn:hover {
            transform: scale(1.2, 1.1);
        }
    </style>
</head>
<body>
    <h1>concatMap Example</h1>
    <button class="btn" id="user1">Fetch User 1</button>
    <button class="btn" id="user2">Fetch User 2</button>
    <button class="btn" id="user3">Fetch User 3</button>

    <script>
        const { fromEvent, concatMap, delay, map } = rxjs;
        const { ajax } = rxjs.ajax;

        const buttons = document.querySelectorAll('.btn');

        const buttonClicks$ = fromEvent(buttons, 'click').pipe(
            concatMap(event => {
                const userId = event.target.getAttribute('id').replace('user', ''); 
                return ajax.getJSON(`https://jsonplaceholder.typicode.com/users/${userId}`).pipe(
                    delay(Math.random() * 2500),
                    map(res => {
                        return {
                            id: userId,
                            result: res
                        };
                    })
                );
            })
        );

        buttonClicks$.subscribe({
            next: data => console.log('Fetched User:', data.id, 'result:', data.result),
            error: err => console.error('Error fetching data:', err)
        });
    </script>
</body>
</html>

Breakdown: Try clicking on each button in a defined sequence as fast as you can, and check the logs.

You will notice that the results are logged in the exact sequence of your clicks, demonstrating how concatMap maintains the order of operations, even when some asynchronous tasks take longer to complete.

Unlike mergeMap, concatMap ensures that once an API call is initiated, subsequent calls wait for the previous one to complete before starting.

This behavior is crucial in scenarios where the order of user interactions must be respected, enhancing the user experience by ensuring that actions are processed sequentially.



3. switchMap - Cancelling Previous Observables for Latest Data

The switchMap operator is particularly useful in scenarios where only the most recent emitted value matters.

When a new value is emitted, switchMap unsubscribes from the previous inner observable, effectively cancelling it and subscribing to the new one.

This behavior is ideal for applications such as search autocompletes or scenarios where user interactions generate frequent updates.


Use Case

Let's maintain the usecase from the previous operators

If a user clicks another button while a previous request is still ongoing, switchMap will cancel the ongoing request and only show the results for the most recent button clicked.


Example

Imagine a user interface with multiple buttons, each corresponding to a different endpoint.

When a user clicks a button, the application fetches data from that endpoint.

If the user clicks another button before the previous request completes, switchMap ensures that the previous request is cancelled.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>RxJS Example: switchMap with Buttons</title>
    <script src="https://unpkg.com/rxjs@^7/dist/bundles/rxjs.umd.min.js"></script>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
            text-align: center;
        }
        .btn {
            display: inline-block;
            padding: 10px 15px;
            margin: 25px;
            background-color: teal;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            transition: all .5s;
        }
        .btn:hover {
            transform: scale(1.1);
        }
    </style>
</head>
<body>
    <h1>switchMap Example</h1>
    <button class="btn" id="posts">Fetch Posts</button>
    <button class="btn" id="comments">Fetch Comments</button>
    <button class="btn" id="users">Fetch Users</button>
    
    <div class="result" id="result"></div>

    <script>
        const { fromEvent, switchMap, map, delay } = rxjs;
        const { ajax } = rxjs.ajax;

        const buttons = document.querySelectorAll('.btn');
        const resultDiv = document.getElementById('result');

        const buttonClicks$ = fromEvent(buttons, 'click').pipe(
            switchMap(event => {
                const endpoint = event.target.getAttribute('id');
                return ajax.getJSON(`https://jsonplaceholder.typicode.com/${endpoint}`).pipe(
                    delay(Math.random() * 2500), // Simulate varying response times
                    map(res => ({
                        type: endpoint,
                        result: res
                    }))
                );
            })
        );

        buttonClicks$.subscribe({
            next: data => {
                console.log('From: ', data.type, 'result: ', data.result);
            },
            error: err => console.error('Error fetching data:', err)
        });
    </script>
</body>
</html>

Breakdown: Click on the buttons to trigger different API requests.

Observe that clicking a new button cancels the request from the previous button click.

The most recent data fetched will be displayed, ensuring the UI reflects the latest user action.



Conclusion

In this article, we’ve explored the most wildly used RxJS combinatory operators— mergeMap, concatMap, and switchMap.

We explored how they can help us tame asynchronous streams with finesse.

Understanding when to use each operator is crucial for crafting efficient and responsive applications.

By embracing these operators and the others, you can enhance your app’s user experience, ensuring that actions like API calls and UI events are handled smoothly and efficiently.

So, are you hooked yet? Grab your favorite snacks, fire up your code editor, and start experimenting with these operators! Who knows, you might just find yourself creating the next big thing in the world of web development.

Let 's close this post with a sweet quote:

Debugging is just like being the detective and the murderer in a same crime movie.

Have fun, and happy coding👨‍💻!

Interested in exploring more 🤓? Check out these Related Posts.