Having trouble deciding between flatMap and concatMap in rxJs?

Having trouble grasping the distinction between flatMap and concatMap in rxJs.

The most enlightening explanation I found was on this Stack Overflow post about the difference between concatMap and flatMap

So, I decided to experiment with it myself.

import "./styles.css";
import { switchMap, flatMap, concatMap } from "rxjs/operators";
import { fromFetch } from "rxjs/fetch";
import { Observable } from "rxjs";

function createObs1() {
  return new Observable<number>((subscriber) => {
    setTimeout(() => {
      subscriber.next(1);
      subscriber.complete();
    }, 900);
  });
}

// Functions createObs2, createObs3, createObs4, createObs5 defined similarly

createObs1()
  .pipe(
    flatMap((resp) => {
      console.log(resp);
      return createObs2();
    }),
    // Additional flatMap calls
  )
  .subscribe((resp) => console.log(resp));

console.log("hellooo");

Implemented the example on this CodeSandbox playground

Queries:

1) Using flatMap should interleave the outputs to log (1,3,2,4,5), but mine always logs sequentially as (1, 2, 3, 4, 5). What could be the error in my understanding or implementation?

2) After adding multiple emitted events in createObs2() and createObs3(), results get mixed up even if using concatMap. Expected single numbers show up multiple times leading to a messy outcome like (1, 2, 33, 3, 2, 22, 3, 33, 4, 5, 4, 3, 4, 5). Why does this happen?

To test on the playground, I simply remove one letter from the last console.log statement ("hello"). After saving the change, the project compiles again and displays the output on the console.

Edit: I turned to flatMap and concatMap trying to replace nested subscriptions in Angular when using the http library.

// Nested subscription example
createObs1().subscribe( (resp1) => {
          console.log(resp1);
          createObs2().subscribe( (resp2) => {
               console.log(resp2);
               // Nested subscribe calls for resp3, resp4, resp5
             })
        })

Answer №1

Your experiment setup may not be robust enough to showcase the disparities between these two operators. When an observable only emits once, there is essentially no discernible distinction between concatMap and flatMap (also known as mergeMap). The discrepancies become apparent when there are multiple emissions.

Let's introduce a new scenario. Let's create a source$ observable that sequentially emits increasing integers every 1 second. Then, within our "Higher Order Mapping Operator" (concatMap & mergeMap), we will return an observable that emits varying times every 1 second before completing.

// emit number every second
const source$ = interval(1000).pipe(map(n => n+1)); 

// function to generate observable emitting specified number of times
function inner$(max: number, description: string): Observable<string> {
  return interval(1000).pipe(
    map(n => `[${description}: inner source ${max}] ${n+1}/${max}`),
    take(max), 
  );
}

We will then define two separate observables based on source$ and inner$; one using concatMap and another using flatMap, and observe the outcomes.

const flatMap$ = source$.pipe(
    flatMap(n => inner$(n, 'flatMap$'))
);

const concatMap$ = source$.pipe(
    concatMap(n => inner$(n, 'concatMap$'))
);

Prior to reviewing the distinctions in output, let's address their shared characteristics. Both operators:

  • subscribe to the observable returned by the provided function
  • emit values from this "inner observable"
  • unsubscribe from the inner observable(s)

The dissimilarity lies in how they produce and manage inner subscriptions:

concatMap - permits only one inner subscription at any given time. It adheres to a sequential approach where it subscribes to one inner observable initially, followed by subsequent ones upon completion. This aligns with the behavior of the static method concat.

flatMap (or mergeMap) - allows multiple concurrent inner subscriptions. It subscribes to inner observables as new emissions arrive, resulting in potentially unordered emissions, akin to the functionality of the static method merge (hence my preference for the name "mergeMap").

See a demonstration of the outputs generated by the above observables concatMap$ and mergeMap$ in this StackBlitz:

https://i.sstatic.net/pNLNCl.png

I hope this explanation clarifies your doubts!

#1 - "use of flatMap should mix the outputs"

The lack of desired results here can be attributed to only a single emission passing through flatMap, meaning there was only one "inner observable" emitting values. As exemplified earlier, having numerous emissions enables multiple independent inner observables under flatMap.

#2 - "...and include the code with multiple emitted events then things get messy."

The chaos you experienced arises from having several inner subscriptions emitting values concurrently.

In regards to utilizing concatMap and encountering mixed output, such unpredictability is unexpected. StackBlitz sometimes exhibits odd behavior with observable emissions while auto-saving is active, potentially retaining outdated subscriptions during refreshes, hence the disorderly console output. The same could apply to Code Sandbox.

#3 - "The reason I have gone to flatMap and concatMap was to find a replacement for nested subscriptions in angular using the http library"

This rationale is sound. Nested subscriptions are best avoided due to uncertainties surrounding the cleanup of inner subscriptions.

For most scenarios involving http calls, I find switchMap to be optimal as it discards emissions from irrelevant inner observables. Consider a situation where a component fetches data via an http call based on an id extracted from a route parameter.

itemId$ = this.activeRoute.params.pipe(
    map(params => params['id']),
    distinctUntilChanged()
);

item$ = this.itemId$.pipe(
    switchMap(id => http.get(`${serverUrl}/items/${id}`)),
    map(response => response.data)
);

The aim with item$ is to emit the current item corresponding to the id in the URL. If the user rapidly clicks a button to navigate to a different item, causing rapid changes in the URL param ahead of data retrieval completion, opting for mergeMap would yield multiple emissions from various http calls. This might result in screen flickering or potential display desynchronization if responses arrive out of order.

With concatMap, users endure a serialized wait for all http calls to conclude, regardless of relevance. Conversely, switchMap swiftly unsubscribes from previous inner observables upon receiving a new emission (new itemId), ensuring emission solely from the latest relevant call.

Note that since http observables emit just once, the choice among operators (switchMap, mergeMap, concatMap) may seem inconsequential, given their automated handling of inner observables. Nonetheless, selecting the ideal operator now future-proofs your code to accommodate potential multiple emissions, guaranteeing anticipated behavior.

Answer №2

Whenever the initial observable emits, a new observable is generated within the flatMap and begins emitting. However, the data from the first observable does not continue to propagate.

Each time the subsequent observable emits, another flatMap creates yet another observable, forming a chain. Once again, the original input received by the flatMap does not get passed along.

createObs1()
  .pipe(
    flatMap(() => createObs2()), // New stream created whenever previous observable emits
    flatMap(() => createObs3()), // New stream created whenever previous observable emits
    flatMap(() => createObs4()), // New stream created whenever previous observable emits
    flatMap(() => createObs5()), // New stream created whenever previous observable emits
  )
  .subscribe((resp) => console.log(resp));

// OUTPUT:
// 5

Therefore, only the values emitted from createObs5() are actually sent to the observer. The emissions from the preceding observables serve as triggers for generating new observables.

If you were to utilize merge, then the outcome would align more with your expectations:

createObs1()
  .pipe(
    merge(createObs2()),
    merge(createObs3()),
    merge(createObs4()),
    merge(createObs5()),
  )
  .subscribe((resp) => console.log(resp));

// OUTPUT:
// 5
// 4
// 3
// 2
// 1

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Troubleshooting a bug in React TypeScript with conditional logic

I have a new message button and a few conversations. When I click on a conversation, the right side should display the message box. Clicking on the new message button should show the create new message box. Here are my useState and handlers: const [newMess ...

Having trouble setting up discord.js on my device, getting an error message that says "Unable to install discord.js /

Trying to install Discord.JS by using the command npm install discord.js seems to be successful at first, but unfortunately it doesn't work as expected. Upon running the index.js file, an error message pops up indicating that the module discord.js is ...

Failure to validate two dates, even when they are both in date format within AngularJS

Although it may seem silly to ask, I am confused as to why this is not working. Even though all the values appear fine in debug mode. My goal is to display an error if the productionStartFrom date is before the current date. Controller scope.currentDate ...

The res.send() method in Restify was not triggered within the callback function of an event

Currently, I am utilizing restify 2.8.4, nodejs 0.10.36, and IBM MQ Light messaging for a RPC pattern. In this setup, when the receiver has the result ready, it emits an event. The below restify POST route is supposed to capture this event along with the ...

Alter the entity type when passing it as a parameter

Trying to alter the Entity type, I am invoking a function that requires two parameters - one being the entity and the other a location. However, when trying to assign a Type for the first entity, it throws an error: Error: Argument of type 'Node<En ...

Isn't AJAX all about the same origin policy?

Despite my confusion surrounding the same domain origin policy and jQuery AJAX, I have noticed that when I make a GET request to a URL using jQuery, I am able to successfully retrieve the results. This goes against what I understood about the restriction ...

directive does not execute when the <Enter> key is pressed

I recently came across a helpful post on Stack Overflow about creating an Enter keypress directive. After following the instructions, here is my JavaScript code that replicates the functionality: JavaScript var app = angular.module('myApp', [] ...

`How can I manage my electron.js application effectively?`

As a newcomer to electron.js, I have successfully created a game using html, css, and javascript that currently runs offline on the client side. However, I am now looking for a way to access, analyze, and make changes to this app. One solution could be lo ...

Dynamic fade effect with GSAP on scroll

Currently, I am attempting to implement a fade out animation with GSAP Scroll Trigger. The aim is for the page to first scroll across the X axis of the title before scrolling up and fading out. While I have made some progress, I am facing an issue where th ...

Enhance the standard input control in Vue.js by extending it to include features such as

Incorporating vue.js: Can you enhance a standard HTML input without the need for a wrapper element? I am interested in customizing a textarea like so: Vue.component('custom-textarea', { data () => { return { } }, template: &apo ...

`Exit function in Vue.js: A step-by-step guide`

Here is a code snippet in vue.js which includes an async function: async up(id, point){ this.change = true const pId = id + '-' + firebase.auth().currentUser.uid db.collection('answer').d ...

Issue: Property is not found within the parameters of the Generic Type

Currently, I am delving into the world of Typescript and have been exploring various exercises that I stumbled upon online. However, I have encountered some trouble with the feedback on incorrect solutions. Specifically, I am facing an issue with the follo ...

React functional components can utilize switch cases for conditional logic

I am attempting to create a multi-step form using a switch case, but for some reason, changing the state with nextPrev does not update the case. export const FormSteps = ({items, pending}) => { const [step, setStep] = useState (2) const nextS ...

Is it possible for me to determine whether a javascript file has been executed?

I am currently working with an express framework on Node.js and I have a requirement to dynamically change the value (increase or decrease) of a variable in my testing module every time the module is executed. Is there a way to determine if the file has ...

Step-by-step guide on making a post request to the Facebook Graph Api with Httparty in a Rails application

I'm currently working on developing a bot for Facebook Messenger, and I need to make a post request call to the Facebook Graph API. The sample code provided by Facebook is in Node.js, but I am working with Rails as my backend framework. Sample Code ...

Tips for retaining filled data after an incomplete form submission

<html> <head><script type="text/javascript"> function pa(){ var fname = document.getElementById('fname').value; var lname = document.getElementById('lname').value; var email = document. ...

Errors persist with Angular 2 iFrame despite attempts at sanitization

Attempting to add an iFrame within my Angular 2 application has been challenging due to the error message that keeps popping up. unsafe value used in a resource URL context The goal is to create a dynamic URL to be passed as a parameter into the iFrame ...

Having numerous bxsliders implemented in a Genesis child theme

I'm currently working on incorporating multiple bxsliders through custom fields in a wp genesis child theme. The initial slider was successfully implemented using the following function within the genesis child theme functions: add_action('genes ...

Tips for saving a Cytoscape.js diagram as an image

Currently, I am diving into learning Cytoscape.js. After successfully creating an HTML file with a basic graph, I attempted to export the graph to an image using the following code: var png64 = cy.png(); // Inserting the png data in an img tag document.qu ...

Ignoring TypeScript overloads after the initial overload declaration

Issue An error occurs when attempting to call an overload method for a specific function. The call only works for the first defined overload, causing an error if the call is made to the second overload with mismatched typing compared to the first overload ...