This is the 25th day of my participation in Gwen Challenge. For more details, see Gwen Challenge.

Map data is a common operation during program development. When you use RxJS in your code to generate data streams, you will most likely end up needing a way to map the data into whatever format you want. RxJS provides regular map functions, as well as functions such as mergeMap, switchMap, and concatMap, which handle them slightly differently.

map

The map operator is the most common. For each value emitted by an Observable, a function can be applied to modify the data. The return value is rereleased as an Observable in the background so that it can continue to be used in the stream. It works in much the same way as using it in arrays.

The difference is that an array is always just an array, and when you map, you get the current index value in the array. In the case of an Observable, the data can be of any type. This means that you may need to do something extra in the Observable Map function to get the desired result. Look at the following example:

import { of } from "rxjs"; import { map } from "rxjs/operators"; // Const data = of([{brand: "Porsche ", model: "911"}, {brand:" Porsche ", model: "macan"}, {brand: "Ferrari ", model: "458"}, {brand: "Lamborghini ", model: "urus"}]; // Output in the brand model format: [porsche 911, Porsche Macan, Ferrari 458, "Lamborghini urus"] data.pipe(map(cars => cars.map(car => '${car.brand} ${car.model}'))).subscribe(cars => console.log(cars)); // Filter data, only retain data with brand as porsche, result: [{" brand ", "porsche", "model" : "911"}, {" brand ":" porsche ", "model" : "macan}] data. The pipe (map (cars = > cars. The filter (car. = > car brand = = = "Porsche ")). Subscribe (cars => console.log(cars));Copy the code

We first created observables with a series of cars. Then subscribe to the observable twice.

  1. The first time you modify the data, you get a bybrandandmodelAn array of concatenated strings.
  2. The second time I modified the data, I got a onlybrandforporscheThe array.

In both cases, the Observable Map operator is used to modify the data emitted by the Observable. Returns the result of the modification, and the map operator encapsulates the result in an observable so that we can subscribe later.

MergeMap

Now suppose you have a scenario where you have an observable object that emits an array, and for each item in the array, you need to get data from the server.

You can do this by subscribing to an array, and then setting up a map to call a function that handles the API call, subscribing to its results. As follows:

import { of, from } from "rxjs"; import { map, delay } from "rxjs/operators"; Const getData = param => {return of(' retrieve parameter: ${param} ').pipe(delay(1000)); }; from([1, 2, 3, 4]) .pipe(map(param => getData(param))) .subscribe(val => console.log(val));Copy the code

The map function returns the value of getData. In this case, it’s observable. But this creates a problem: there is now an extra observable to deal with.

To further clarify this point: from([1,2,3,4]) is the “external” observable, and the result of getData() is the “internal” observable. Theoretically, both external and internal observable data must be accepted. It could be something like this:

import { of, from } from "rxjs"; import { map, delay } from "rxjs/operators"; Const getData = param => {return of(' retrieve parameter: ${param} ').pipe(delay(1000)); }; from([1, 2, 3, 4]) .pipe(map(param => getData(param))) .subscribe(val => val.subscribe(data => console.log(data)));Copy the code

As you can imagine, this is a far cry from the ideal situation where you have to call Subscribe twice. This is where mergeMap comes in. MergeMap is essentially a combination of mergeAll and Map. MergeAll is responsible for subscribing to “internal” observables, and when MergeAll merges the values of “internal” observables into “external” observables, it no longer needs to subscribe twice. As follows:

import { of, from } from "rxjs"; import { map, delay, mergeAll } from "rxjs/operators"; Const getData = param => {return of(' retrieve parameter: ${param} ').pipe(delay(1000)); }; from([1, 2, 3, 4]) .pipe( map(param => getData(param)), mergeAll() ) .subscribe(val => console.log(val));Copy the code

This is much better, and mergeMap would be the best solution to this problem. Here’s the full example:

import { of, from } from "rxjs"; import { map, mergeMap, delay, mergeAll } from "rxjs/operators"; Const getData = param => {return of(' retrieve parameter: ${param} ').pipe(delay(1000)); }; // use map from([1, 2, 3, 4]) .pipe(map(param => getData(param))) .subscribe(val => val.subscribe(data => console.log(data))); // Use map and mergeAll from([1, 2, 3, 4]). Pipe (map(param => getData(param)), mergeAll() ) .subscribe(val => console.log(val)); // use mergeMap from([1, 2, 3, 4]).pipe(mergeMap(param => getData(param))).subscribe(val => console.log(val));Copy the code

SwitchMap

SwitchMap has similar behavior and will subscribe to internal observables as well. However, a switchMap is a combination of switchAll and Map. SwitchAll unsubscribes the previous subscription and subscribes the new subscription. In the scenario above, you want to make API calls for each item in the “external” observable array, but switchMap doesn’t work very well because it will cancel the first three subscriptions and only process the last one. That means there’s only one result. The full example can be seen here:

import { of, from } from "rxjs"; import { map, delay, switchAll, switchMap } from "rxjs/operators"; const getData = param => { return of(`retrieved new data with param ${param}`).pipe(delay(1000)); }; // Use a regular map from([1, 2, 3, 4]) .pipe(map(param => getData(param))) .subscribe(val => val.subscribe(data => console.log(data))); // Use map and switchAll from([1, 2, 3, 4]). Pipe (map(param => getData(param)), switchAll() ) .subscribe(val => console.log(val)); // Use switchMap from([1, 2, 3, 4]).pipe(switchMap(param => getData(param))).subscribe(val => console.log(val));Copy the code

Although switchMap is not suitable for the current scenario, it is suitable for other scenarios. It comes in handy, for example, if you combine a list of filters into a data stream and make an API call when you change the filter. If the previous filter change is still being processed and the new change has been made, it will cancel the previous subscription and start the new subscription on the latest change. An example can be seen here:

import { of, from, BehaviorSubject } from "rxjs"; import { map, delay, switchAll, switchMap } from "rxjs/operators"; const filters = ["brand=porsche", "model=911", "horsepower=389", "color=red"]; const activeFilters = new BehaviorSubject(""); Const getData = params => {return of(' receiving: ${params} ').pipe(delay(1000)); }; const applyFilters = () => { filters.forEach((filter, index) => { let newFilters = activeFilters.value; if (index === 0) { newFilters = `? ${filter}`; } else { newFilters = `${newFilters}&${filter}`; } activeFilters.next(newFilters); }); }; // Use switchMap activeFilters. Pipe (switchMap(param => getData(param))).subscribe(val => console.log(val)); applyFilters();Copy the code

As you can see in the console, getData records all parameters only once. Saved 3 API calls.

ConcatMap

The last example is concatMap. ConcatMap subscribes to internal observables. But unlike switchMap, if a new observation object comes in, it unsubscribes to the current observation object, and concatMap does not subscribe to the next observation object until the current observation object completes. This has the advantage of preserving the order in which the observable emits the signal. To demonstrate this:

import { of, from } from "rxjs"; import { map, delay, mergeMap, concatMap } from "rxjs/operators"; const getData = param => { const delayTime = Math.floor(Math.random() * 10000) + 1; Return of(${param} and delay: ${delayTime} '). Pipe (delay(delayTime)); }; // use map from([1, 2, 3, 4]) .pipe(map(param => getData(param))) .subscribe(val => val.subscribe(data => console.log("map:", data))); // use mergeMap from([1, 2, 3, 4]).pipe(mergeMap(param => getData(param))).subscribe(val => console.log("mergeMap:", val)); // Use concatMap from([1, 2, 3, 4]).pipe(concatMap(param => getData(param))).subscribe(val => console.log("concatMap:", val));Copy the code

The getData function has a random delay between 1 and 10000 milliseconds. Through the browser logs, you can see that the Map and mergeMap operators will record any values returned, not in the original order. ConcatMap records have the same values as when they started.

conclusion

Mapping data to the desired format is a common task. RxJS comes with some very compact operators that do the job just fine.

To recap: Map is used to map normal values into whatever format you want. The return value is again wrapped in an observable, so it can continue to be used in the data flow. It is easier to use mergeMap, switchMap, or concatMap when you must work with an “internal” observation object. If you just want to convert data into an Observable, use mergeMap. To discard the old Observable and keep the new One, use switchMap. If you need to convert data to an Observable and keep the order, use concatMap.