0
votes

I'm trying to return multiple actions from a single effect, but when using the map operator it gives me the following error:

Type 'Observable<[TotalStatisticDto[], DailyStatisticDto[], DailyStatisticDto[]]>' must have a 'Symbol.iterator' method that returns an iterator.

Switching map with switchMap fixes the error and everything works.

Could someone explain to me what is the difference between the two operators in this situation?

 getStatistics$ = createEffect(() => this.actions$.pipe(
    ofType(StatisticsActions.fetchTotal),
    map(() => {
        const total$ = this.statisticsService.getTotal();
        const daily$ = this.statisticsService.getDaily();
        const totalDaily$ = this.statisticsService.getTotalDaily();
        return combineLatest([total$, daily$, totalDaily$]);
    }),
    concatMap(([total, daily, totalDaily]) => [
        StatisticsActions.fetchTotalSuccess({ statistics: total }),
        StatisticsActions.fetchDailySuccess({ statistics: daily }),
        StatisticsActions.fetchTotalDailySuccess({ statistics: totalDaily })
    ])));
3

3 Answers

2
votes

An Observable is a stream of "events".
map is for changing the content of the events.
switchMap changes the stream, it switches to another observable.

You are returning combineLatest. That means you are returning an observable. Therefor map will return in an error.

Perhaps take a look at "https://rxjs-dev.firebaseapp.com/api/operators/" to get a deeper understanding of the different operators.

Added clarifification:
An example. The source stream emits each second a counting value
1, 2, 3, 4, 5, 6, ... If you would apply a map( value => value * 2) (just multipling the content of the event), then map returns an observable that "resends" each incoming event and just double the value. Therefor the subscriber would get each second a value
2, 4, 6, 8, 10, ...
So, the frequency is the same, just the content of each event is changed.

If you use switchmap, then you switch the observable. For example if we use again the source stream (1,2,3,4,5,... each second) and we would then switchMap it to a REST request which should get weather data for your home town, then each second a new weather request is triggered and the subscriber will not see anything from the 1,2,3,4,5, ... but just get weather data.

0
votes

Let's look at the difference between mergeMap and map


A Quick Aside That Will Come in Handy Later

Here's a custom observable that emits 2 random numbers:

function getCustomeObservable(min, max){
  console.log(`Hello From getCustomObservable(${min},${max})`);

  const getRndInteger = () => 
    Math.floor(Math.random() * (max - min + 1) ) + min;

  return new Observable(observer => {
    console.log(`Hello From The Observable created by getCustomObservable(${min},${max})`);
    for(let i = 0; i < 2; i++){
        observer.next(getRndInteger())
    }
  });
}

Lets I have a bunch of tuples that I want to add together in a stream, then subscibe and print the added up numbers to the console. That might look like this:

const tuples = [[2,5],[4,7],[6,9]];
const tuples$ = from(tuples);
const added$ = tuples$.pipe(
  map(([a,b]) => a + b)
)
added$.subscribe(console.log); // 7, 11, 15

This prints out 7, 11, and 15. As you can see, map(lambda) is taking the observable tuples$ as input and returning the observable added$ as output. (pipe isn't really composing anything in this case so we'll just ignore it for this explanation). How does map know how to turn one stream into the other? It just applies the lambda to each value in the stream.

Okay, so what does this do?

const tuples = [[2,5],[4,7],[6,9]];
const tuples$ = from(tuples);
const rando$ = tuples$.pipe(
  map(([a,b]) => getCustomeObservable(a, b))
)
rando$.subscribe(console.log);

The output will look something like this (depends on your browser log):

Observable: {isScalar: false}
Observable: {isScalar: false}
Observable: {isScalar: false}

This makes sense. Every tuple is used to create an observable. So the values in the rando$ stream are observables. Actually, our output is a bit different, it will look like this because our custom observable has some console logging too:

Hello From getCustomObservable(2,5)
Observable: {isScalar: false}
Hello From getCustomObservable(4,7)
Observable: {isScalar: false}
Hello From getCustomObservable(6,9)
Observable: {isScalar: false}

What you'll notice is that our observables are never run. The console message "Hello From The Observable created by [...]" never prints to the console. This is because we've created a bunch of observables, but we've never subscribed to them. How do we subscribe to them? Well, there are many ways actually.

const tuples = [[2,5],[4,7],[6,9]];
const tuples$ = from(tuples);
const rando$ = tuples$.pipe(
  map(([a,b]) => getCustomeObservable(a, b)),
  mergeAll()
)
rando$.subscribe(console.log);

mergeAll() expects the values of the stream to be observables. Which we could see the output of map(lambda) was creating. It then subscribes to all the incoming observables (running them) and merges their output.

Console:

Hello From getCustomObservable(2,5)
Hello From The Observable created by getCustomObservable(2,5)
4
3
Hello From getCustomObservable(4,7)
Hello From The Observable created by getCustomObservable(4,7)
6
7
Hello From getCustomObservable(6,9)
Hello From The Observable created by getCustomObservable(6,9)
7
8

This does the same thing:

const tuples = [[2,5],[4,7],[6,9]];
const tuples$ = from(tuples);
const rando$ = tuples$.pipe(
  mergeMap(([a,b]) => getCustomeObservable(a, b))
)
rando$.subscribe(console.log);

Unlike map, mergeMap expects its lambda to return an observable. mergeMap subscribes to that observable and creates a stream with the outputs of that observable. When new values arrive, it expects more observables and subscribes to those, and merges their outputs together.

The console output is identical in this case to how it was with mergeAll

Here's another way to merge streams, though it'll have a slightly different order to the console output.

const tuples = [[2,5],[4,7],[6,9]];
const tuples$ = from(tuples);
const rando$ = tuples$.pipe(
  map(([a,b]) => getCustomeObservable(a, b)),
  toArray(),
  mergeMap(arrayOfObservables => merge(...arrayOfObservables))
)
rando$.subscribe(console.log);

console output:

Hello From getCustomObservable(2,5)
Hello From getCustomObservable(4,7)
Hello From getCustomObservable(6,9)
Hello From The Observable created by getCustomObservable(2,5)
2
3
Hello From The Observable created by getCustomObservable(4,7)
7
4
Hello From The Observable created by getCustomObservable(6,9)
9
8

This time we collected all the observables in an array (toArray), then we merged the array of observables into a single observable with merge and then we subscribed to that single merged observable with mergeMap.

You'll notice in the console output that all three observables are created before the first one is run (subscribed to). RxJS observables do not generate values until subscribed to.


TlDr:

It is the merge part of mergeMap that makes it different from map. Merge, concat, exhaust, race, zip, switch, ect are all special ways of subscribing to a list of observables.


Aside For an RxJS Antipattern

const tuples = [[2,5],[4,7],[6,9]];
const tuples$ = from(tuples);
const rando$ = tuples$.pipe(
  map(([a,b]) => getCustomeObservable(a, b)),
)
rando$.subscribe(val$ => 
  val$.subscribe(console.log)
);

This is a way to merge your streams without using merge, mergeMap, or mergeAll. Oftentimes, when you're first learning RxJS, this feels like an intuitive approach. You're used to getting the output of a stream with a call to .subscribe(LAMBDA).

Nesting subscriptions is, however, an antipattern for good reason. "Oh, the tangled webs you'll weave!".

The biggest problem is that nested subscriptions are very close to merging, but pretty far from the other joining operators like concat, zip, exhaust, ect. Having all the joining operators used similarly makes a world of difference when maintaining/extending code.

The next is that streams can be multicasted, shared, replayed, ect - none of the logic found within a subscription can be part of that process. So data-processing steps are well served to remain within operators (custom or otherwise).

0
votes

Alternatively:

 getStatistics$ = createEffect(() => this.actions$.pipe(
    ofType(StatisticsActions.fetchTotal),
    mergeMap(() => {
        const total$ = this.statisticsService.getTotal();
        const daily$ = this.statisticsService.getDaily();
        const totalDaily$ = this.statisticsService.getTotalDaily();
        return [
           total$.map(t => StatisticsActions.fetchTotalSuccess({ statistics: t})),
           daily$.map(t => StatisticsActions.fetchDailySuccess({ statistics: t})),
           totalDaily$.map(t => StatisticsActions.fetchTotalDailySuccess({ statistics: t)) 
        ];
    }),
    mergeAll()
));