getting started with MergeMap Rxjs operators

I spend hours trying to understand mergeMap Rxjs operator because I never found a good tutorial starting from a simple example to a more complex one. So in this tutorial, I will try to explain the mergeMap Rxjs operator from the very beginning.

When and why do we use MergeMap?

MergeMap operator allows you to flatten an inner observable (by the way the other operator for mergeMap is flatMap). It can be use each time you have an observable and you want to turn it into another one. In that way, we merge the first value emitted into the second operation through a merge function. So, let’s see a basic example of mergeMap Rxjs operator.

Basic examples

import {Component, OnInit} from '@angular/core';
import {of} from 'rxjs';
import {mergeMap} from 'rxjs/operators';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {

  constructor() {
  }

  ngOnInit(): void {
    const source = of('Hello')
      .pipe(
        mergeMap((value) => of(value + ' World!')));
    source.subscribe((value) => {
      console.log(value);
    });
  }

}

This example shows you a simple case of using mergeMap operator. This case is quite simple. Indeed, the ‘Of(‘Hello’)’ function is an observable which emits the value ‘Hello’. It appears we would like to display ‘Hello World!’. To achieve this, we turn the first observable into another one by merging them. So, mergeMap takes the first value and create a new Observable by returning ‘of(value + ‘ World!’)’. So with no suspens, in the console we see ‘Hello World!’. Let’s take another example a little bit more complex:

import {Component, OnInit} from '@angular/core';
import {interval, of} from 'rxjs';
import {map, mergeMap} from 'rxjs/operators';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {

  constructor() {
  }

  ngOnInit(): void {
    const source = of([1, 2, 3, 4, 5, 6, 7])
      .pipe(
        mergeMap((value) => {
      return interval(1000)
        .pipe(
          map(currentSecond => value.map(currentNumber => currentNumber * currentSecond))
    );
    })
  );
    source.subscribe((value) => {
      console.log(value);
      /*  result:
          [0,0,0,0,0,0,0]
          [1,2,3,4,5,6,7]
          [2,4,6,8,10,12,14]
          [3,6,9,12,15,18,21]
          etc....
      */
    });
  }
}

This example shows us a better use case of mergeMap operator. One important thing to notice is the memory of the last value emitted. Let me explain my words. At the very beginning, the first observable emits [1, 2, 3, 4, 5, 6, 7], then the second one emits 0 so the returns after using mergeMap is basically [0,0,0,0,0,0,0]. But the second value emission is quite interesting because only the second observable emits values, so to calculate the second return of the mergeMap operator, the operation takes the last value in memory for the first observable and then the result is [1 *1, 2 *1, 3*1, 4*1, 5*1, 6*1, 7*1] = [1, 2, 3, 4, 5, 6, 7]. If you do understand what I just said you should understand the complexity of the next example:

import {Component, OnInit} from '@angular/core';
import {interval} from 'rxjs';
import {map, mergeMap} from 'rxjs/operators';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {

  constructor() {
  }

  ngOnInit(): void {
    const source = interval(5000)
      .pipe(
        mergeMap((value1) => {
          return interval(1000)
            .pipe(map(value2 => {
              console.log('value1:' + value1);
              console.log('value2:' + value2);
              return value1 * value2;
            }));
        })
      );
    source.subscribe((value) => {
      console.log(value);
    });
  }

}

This example shows you the complexity of observables. But don’t worry, let’s do it slowly. After 5 seconds, the first value (here 0) is emitted but we have to wait one more second to have the first result. So after 6 seconds the console will log 0. At this point we are at the 6th seconds, then here is the result:

  • 7th second:
    • value1: 0
    • value2: 1
    • result: 0
  • 8th second:
    • value1: 0
    • value2: 2
    • result: 0
  • 9th second:
    • value1: 0
    • value2: 3
    • result: 0

Then for the 10th second, a lot of things happens in a concurrent way. The two observables emit their value. This is the result:

  • 10th second:
    • interval(5000) emits a value and here is the result after one more second:
      • value1: 1
      • value2: 0
      • result: 0
    • interval(1000) emits a value:
      • value1: 0
      • value2: 4
      • result: 0

For the 10th second, the two observables emit at the same time creating a concurrent emission. But here is the 1million dollar question:

when the interval(1000) emits, why the value of the value1 is 0 and not 1?

Indeed, there is a little subtlety in this case. Remember, when one observable emits a value in a mergeMap operator, to calculate the result, we take the last value emitted. Here, the last value emitted is 0 because when the interval(5000) emits its value we have to wait one more second to complete the observable due to the emission of interval(1000). So the last value emitted and completed is 0.

But there is another thing you should grasp with this case. When there is a concurrent emission in mergeMap operator, both are processed without cancelling the previous one (that’s what happen in the 10th second). This can easily create a leak of memory. Be aware of this when you are using mergeMap operator(or consider using switchMap).

Concrete example

If you do understand my last example. This use case should be easy. We want to build two forms to make one digit addition by using mergeMap operator. And of course, there is no bouton to validate the operation, we cannot use ngModel and the addition is directly made when we fill the second form and nothing happens when we fill the first one. Here is the result:


<input id="op1" #op1 [maxLength]="1"/>
<input id="op2" #op2 [maxLength]="1"/>
result: {{ result }}

import {AfterViewInit, Component, ElementRef, ViewChild} from '@angular/core';
import {fromEvent} from 'rxjs';
import {map, mergeMap} from 'rxjs/operators';
import {isNumeric} from 'rxjs/internal-compatibility';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit {

  result: number;

  @ViewChild('op1', { static: false }) op1: ElementRef<HTMLInputElement>;
  @ViewChild('op2', { static: false }) op2: ElementRef<HTMLInputElement>;

  constructor() {
  }

  ngAfterViewInit(): void {
    fromEvent<KeyboardEvent>(this.op1.nativeElement, 'keydown')
      .pipe(
        mergeMap( event1 => fromEvent<KeyboardEvent>(this.op2.nativeElement, 'keydown')
          .pipe(map(event2 => {
            if (isNumeric(event1.key) && isNumeric(event2.key)) {
              return Number(event1.key) + Number(event2.key);
            }
          })))
      )
      .subscribe(res => {
        this.result = res;
      });
  }

}

With no surprise, when we fill the first input nothing happens and it’s only when the second one is filled the operation is made.

Conclusion

  • MergeMap RxJs operator allows you to merge two observables into one
  • Be aware of concurrent emissions and memory leaks

Be the first to comment

Leave a Reply

Your email address will not be published.


*