Struggling with flatMap vs concatMap in rxJs - javascript

I am struggling to understand the difference between the flatMap and concatMap in rxJs.
The most clear answer that I could understand was that here difference-between-concatmap-and-flatmap
So I went and tried things out by my self.
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);
});
}
function createObs2() {
return new Observable<number>((subscriber) => {
setTimeout(() => {
subscriber.next(2);
//subscriber.next(22);
//subscriber.next(222);
subscriber.complete();
}, 800);
});
}
function createObs3() {
return new Observable<number>((subscriber) => {
setTimeout(() => {
subscriber.next(3);
//subscriber.next(33);
//subscriber.next(333);
subscriber.complete();
}, 700);
});
}
function createObs4() {
return new Observable<number>((subscriber) => {
setTimeout(() => {
subscriber.next(4);
subscriber.complete();
}, 600);
});
}
function createObs5() {
return new Observable<number>((subscriber) => {
setTimeout(() => {
subscriber.next(5);
subscriber.complete();
}, 500);
});
}
createObs1()
.pipe(
flatMap((resp) => {
console.log(resp);
return createObs2();
}),
flatMap((resp) => {
console.log(resp);
return createObs3();
}),
flatMap((resp) => {
console.log(resp);
return createObs4();
}),
flatMap((resp) => {
console.log(resp);
return createObs5();
})
)
.subscribe((resp) => console.log(resp));
console.log("hellooo");
I have used that playground here playground example
Questions
1)
From my understanding the use of flatMap should mix the outputs so that the console logs are like (1,3,2,4,5). I have tried more than 30 times and always come on the same row (1, 2, 3, 4, 5)
What am I doing wrong or have undestood wrong?
2)
If on createObs2() and createObs3() you remove the comments and include the code with multiple emitted events then things get messy. Even if you change to concatMap it messes things and results come mixed. Multiple numbers that I expect only once come multiple times. The result can be (1, 2, 33, 3, 2, 22, 3, 33, 4, 5, 4, 3, 4, 5) Why this happens?
How I test the example on playground. I just remove only 1 letter from the last console.log("hello"). Only one change for example console.log("heloo") and is then observed and project is compiled again and output printed in console.
Edit: The reason I have gone to flatMap and concatMap was to find a replacement for nested subscriptions in angular using the http library.
createObs1().subscribe( (resp1) => {
console.log(resp1);
createObs2().subscribe( (resp2) => {
console.log(resp2);
createObs3().subscribe( (resp3) => {
console.log(resp3);
createObs4().subscribe( (resp4) => {
console.log(resp4);
createObs5().subscribe( (resp5) => {
console.log(resp5);
})
})
})
})
})

Your test scenario is not really sufficient to see the differences between these two operators. In your test case, each observable only emits 1 time. If an observable only emits a single value, there is really no different between concatMap and flatMap (aka mergeMap). The differences can only be seen when there are multiple emissions.
So, let's use a different scenario. Let's have a source$ observable that simply emits an incrementing integer every 1 second. Then, within our "Higher Order Mapping Operator" (concatMap & mergeMap), we will return an observable that emits a variable number of times every 1 second, then completes.
// emit number every second
const source$ = interval(1000).pipe(map(n => n+1));
// helper to return observable that emits the provided 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),
);
}
Then let's define two separate observables based on the source$ and the inner$; one using concatMap and one using flatMap and observe the output.
const flatMap$ = source$.pipe(
flatMap(n => inner$(n, 'flatMap$'))
);
const concatMap$ = source$.pipe(
concatMap(n => inner$(n, 'concatMap$'))
);
Before looking the differences in the output, let's talk about what these operators have in common. They both:
subscribe to the observable returned by the passed in function
emit emissions from this "inner observable"
unsubscribe from the inner observable(s)
What's different, is how they create and manage inner subscriptions:
concatMap - only allows a single inner subscription at a time. As it receives emissions, it will only subscribe to one inner observable at a time. So it will initially subscribe to the observable created by "emission 1", and only after it completes, will it subscribe to the observable created by "emission 2". This is consistent with how the concat static method behaves.
flatMap (aka mergeMap) - allows many inner subscriptions. So, it will subscribe to the inner observables as new emissions are received. This means that emissions will not be in any particular order as it will emit whenever any of its inner observables emit. This is consistent with how the merge static method behaves (which is why I personally prefer the name "mergeMap").
Here's a StackBlitz that shows the output for the above observables concatMap$ and mergeMap$:
Hopefully, the above explanation helps to clear up your questions!
#1 - "use of flatMap should mix the outputs"
The reason this wasn't working as you expected was because only one emission was going through the flatMap, which means you only ever had a single "inner observable" emitting values. As demonstrated in the above example, once flatMap receives multiple emissions, it can have multiple inner observables that emit independently.
#2 - "...and include the code with multiple emitted events then things get messy."
The "things get messy" is due to having multiple inner subscription that emit values.
For the part you mention about using concatMap and still getting "mixed" output, I would not expect that. I have seen weird behavior in StackBlitz with observable emissions when "auto save" is enabled (seems like sometimes it doesn't completely refresh and old subscriptions seem to survive the auto refresh, which gives very messy console output). Maybe code sandbox has a similar problem.
#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 makes sense. You don't want to mess around with nested subscriptions, because there isn't a great way to guarantee the inner subscriptions will be cleaned up.
In most cases with http calls, I find that switchMap is the ideal choice because it will drop emissions from inner observables you no longer care about. Imagine you have a component that reads an id from a route param. It uses this id to make an http call to fetch data.
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)
);
We want item$ to emit only the "current item" (corresponds to the id in the url). Say our UI has a button the user can click to navigate to the next item by id and your app finds itself with a click-happy user who keeps smashing that button, which changes the url param even faster than the http call can return the data.
If we chose mergeMap, we would end up with many inner observables that would emit the results of all of those http calls. At best, the screen will flicker as all those different calls come back. At worst (if the calls came back out of order) the UI would be left displaying data that isn't in sync with the id in the url :-(
If we chose concatMap, the user would be forced to wait for all the http calls to be completed in series, even though we only care about that most recent one.
But, with switchMap, whenever a new emission (itemId) is received, it will unsubscribe from the previous inner observable and subscribe to the new one. This means it will not ever emit the results from the old http calls that are no longer relevant. :-)
One thing to note is that since http observables only emit once, the choice between the various operators (switchMap, mergeMap, concatMap) may not seem to make a difference, since they all perform the "inner observable handling" for us. However, it's best to future-proof your code and choose the one that truly gives you the behavior you would want, should you start receiving more than a single emission.

Every time the first observable emits, a second observable is created in the flatMap and starts emitting. However, the value from the first observable is not passed along any further.
Every time that second observable emits, the next flatMap creates a third observable, and so on. Again, the original value coming into the flatMap is not passed along any further.
createObs1()
.pipe(
flatMap(() => createObs2()), // Merge this stream every time prev observable emits
flatMap(() => createObs3()), // Merge this stream every time prev observable emits
flatMap(() => createObs4()), // Merge this stream every time prev observable emits
flatMap(() => createObs5()), // Merge this stream every time prev observable emits
)
.subscribe((resp) => console.log(resp));
// OUTPUT:
// 5
So, it's only the values emitted from createObs5() that actually get emitted to the observer. The values emitted from the previous observables have just been triggering the creation of new observables.
If you were to use merge, then you would get what you may have been expecting:
createObs1()
.pipe(
merge(createObs2()),
merge(createObs3()),
merge(createObs4()),
merge(createObs5()),
)
.subscribe((resp) => console.log(resp));
// OUTPUT:
// 5
// 4
// 3
// 2
// 1

Related

Why does the rxjs share operator not work as expected in this setTimeout() example?

I don't understand why the rxjs share operator does not work with setTimeout().
I'm trying to understand this blogpost. In this example, the concept of "shared subscription" does not seem to work as expected.
const observable1 = Observable.create(observer => {
observer.next(`I am alive.`);
setTimeout(() => {
observer.next(`I am alive again.`);
}, 1000);
}).pipe(share());
observable1.subscribe(x => console.log(x));
observable1.subscribe(x => console.log(x));
Expected:
I am alive.
I am alive again.
Actual:
I am alive.
I am alive again.
I am alive again.
Reproducable stackblitz.
That is the expected output.
From official docs on share() operator:
Returns a new Observable that multicasts (shares) the original Observable. As long as there is at least one Subscriber this Observable will be subscribed and emitting data.
That means as soon as an observer subscribes, the observable starts emitting data.
So when the first subscribe statement observable1.subscribe(x => console.log(x)); executes, an observer subscribes and data is emitted by observer.next('I am alive.); statement.
When second subscribe statement executes, another observer subscribes and it receives only the data emitted from that point of time. This is the data emitted by observer.next('I am alive again.'); in setTimeout() method.
We can see this clearly in this StackBlitz demo where we are logging Observer1 and Observer2 text along with the received data.
I think the point of confusion is seeing two I am alive again. statements. It is logged twice because we are logging it in each subscriber. Move these log statements to the observable and they will only be logged once. This makes it more evident that the observable is executed only once.
This is the supposed behaviour of share(). It monitores and shares only one action. Here is an example taken from learnrxjs.com. As you can see only the tap()-operator is monitored. The mapTo()-operator is ignored.
// RxJS v6+
import { timer } from 'rxjs';
import { tap, mapTo, share } from 'rxjs/operators';
//emit value in 1s
const source = timer(1000);
//log side effect, emit result
const example = source.pipe(
tap(() => console.log('***SIDE EFFECT***')),
mapTo('***RESULT***')
);
/*
***NOT SHARED, SIDE EFFECT WILL BE EXECUTED
TWICE***
output:
"***SIDE EFFECT***"
"***RESULT***"
"***SIDE EFFECT***"
"***RESULT***"
*/
const subscribe = example.subscribe(val => console.log(val));
const subscribeTwo = example.subscribe(val => console.log(val));
//share observable among subscribers
const sharedExample = example.pipe(share());
/*
***SHARED, SIDE EFFECT EXECUTED ONCE***
output:
"***SIDE EFFECT***"
"***RESULT***"
"***RESULT***"
*/
const subscribeThree = sharedExample.subscribe(val => console.log(val));
const subscribeFour = sharedExample.subscribe(val => console.log(val));

Rxjs nested subscribe with multiple inner subscriptions

Original promise based code I'm trying to rewrite:
parentPromise
.then((parentResult) => {
childPromise1
.then(child1Result => child1Handler(parentResult, child1Result));
childPromise2
.then(child1Result => child2Handler(parentResult, child2Result));
childPromise3
.then(child1Result => child3Handler(parentResult, child3Result));
});
I'm trying to figure a way how to avoid the nested subscriptions anti-pattern in the following scenario:
parent$
.pipe(takeUntil(onDestroy$))
.subscribe((parentResult) => {
child1$
.pipe(takeUntil(onDestroy$))
.subscribe(child1Result => child1Handler(parentResult, child1Result));
child2$
.pipe(takeUntil(onDestroy$))
.subscribe(child2Result => child2Handler(parentResult, child2Result));
child3$
.pipe(takeUntil(onDestroy$))
.subscribe(child3Result => child3Handler(parentResult, child3Result));
});
What would be the correct 'RxJS way' to do this?
That seems pretty strange to me. You're creating new subscription for each child every time parentResult arrives. Even though those eventually indeed will be destroyed (assuming onDestroy$ implementation is correct), seems wrong.
You probably want withLatestFrom(parent$) and three separate pipes for each child.
It might look something like:
child1$.pipe(takeUntil(globalDeath$), withLatestFrom(parent$)).subscribe(([childResult, parentResult]) => ...). Not sure if my JS is correct, can't test it at the moment; but the point is: you're getting the latest result from the parent$ every time child1$ fires. Note that you can reverse the direction if necessary (withLatestFrom(child1$)).
You can: 1) pass parent$ through share, and 2) use flatMap three times, something like:
const sharedParent$ = parent$.pipe(share());
sharedParent$.pipe(
flatMap(parentResult => forkJoin(of(parentResult), child1$)),
takeUntil(onDestroy$)),
.subscribe((results) => child1Handler(...results)); // repeat for all children
(If there's more than 2 children, extracting that into a function with child stream and handler as parameters is a good idea).
That's following the original behavior of waiting with subscribing children until parent$ emits. If you don't need that, you can skip flatMap and just forkJoin sharedParent$ and children.
How about using higher order observables? Something like this:
const parentReplay$ = parent$.pipe(shareReplay(1));
of(
[child1$, child1Handler],
[child2$, child2Handler],
[child3$, child3Handler]
).pipe(
mergeMap([child$, handler] => parentReplay$.pipe(
mergeMap(parentResult => child$.pipe(
tap(childResult => handler(parentResult, childResult))
)
)
).subscribe();
If you were using Promises then the corresponding Observables emit only once and then complete.
If this is the case, you can use forkJoin to execute in parallel the child Observables.
So the code could look like
parent$.pipe(
takeUntil(onDestroy$),
// wait for parent$ to emit and then move on
// the following forkJoin executes the child observables in parallel and emit when all children complete - the value emitted is an array with the 3 notifications coming from the child observables
concatMap(parentResult => forkJoin(child1$, child2$, child3$)).pipe(
// map returns both the parent and the children notificiations
map(childrenResults => ({parentResult, childrenResults})
)
).subscribe(
({parentResult, childrenResults}) => {
child1Handler(parentResult, childrenResults[0]);
child1Handler(parentResult, childrenResults[1]);
child1Handler(parentResult, childrenResults[2]);
}
)

RxJS share parent observable among partitioned child observables

I'm coding a game in which the character can fire their weapon.
I want different things to happen when the player tries to fire, depending on whether they have ammo.
I reduced my issue down to the following code (btw I'm not sure why SO's snippet feature does not work, so I made CodePen where you can try out my code).
const { from, merge } = rxjs;
const { partition, share, tap } = rxjs.operators;
let hasAmmo = true;
const [ fire$, noAmmo$ ] = from([true]).pipe(
share(),
partition(() => hasAmmo),
);
merge(
fire$.pipe(
tap(() => {
hasAmmo = false;
console.log('boom');
}),
),
noAmmo$.pipe(
tap(() => {
console.log('bam');
}),
)
).subscribe({
next: val => console.log('next', val),
error: val => console.log('error', val),
complete: val => console.log('complete', val),
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.3.3/rxjs.umd.js"></script>
When I run this code I get the following:
"boom"
"next" true
"bam"
"next" true
"complete" undefined
I don't understand why I get a "bam".
The first emission goes to fire$ (I get a "boom"), which makes sense because hasAmmo is true. But as a side-effect of fire$ emitting is that the result of the partition condition changes, which I guess is causing me to get a "bam".
Am I not supposed to cause side-effects that affect partition()?
Or maybe is there an issue with the way I share() my parent observable? I may be wrong but I would intuitively think the fire$ and noAmmo$ internally subscribe to the parent in order to split it, in which case share() should work?
It actually works correctly. The confusion comes from the partition operator which is basically just two filter operators.
If you rewrite it without partition it looks like this:
const fire$ = from([true]).pipe(
share(),
filter(() => hasAmmo),
);
const noAmmo$ = from([true]).pipe(
share(),
filter(() => !hasAmmo),
);
Be aware that changing hasAmmo has no effect on partition itself. partition acts only when it receives a value from its source Observable.
When you later use merge() it makes two separate subscriptions to two completely different chains with two different from([true])s. This means that true is passed to both fire$ and noAmmo$.
So share() has no effect here. If you want to share it you'll have to wrap from before using it on fire$ and noAmmo$. If the source Observable is just from it's unfortunately going to be even more confusing because the initial emission will arrive only to the first subscriber which is fire$ later when used in merge:
const shared$ = from([true]).pipe(
share(),
);
const fire$ = shared$.pipe(...);
const noAmmo$ = shared$.pipe(...);
The last thing why you're receiving both messages is that partition doesn't modify the value that goes through. It only decides which one of the returned Observable will reemit it.
Btw, rather avoid partition completely because it's probably going to be deprecated and just use filter which is more obvious:
https://github.com/ReactiveX/rxjs/issues/3797
https://github.com/ReactiveX/rxjs/issues/3807

RxJS Subscribe with two arguments

I have two observables which I want to combine and in subscribe use either both arguments or only one. I tried .ForkJoin, .merge, .concat but could not achieve the behaviour I'm looking for.
Example:
obs1: Observable<int>;
obs2: Observable<Boolean>;
save(): Observable<any> {
return obs1.concat(obs2);
}
Then when using this function:
service.save().subscribe((first, second) => {
console.log(first); // int e.g. 1000
console.log(second); // Boolean, e.g. true
});
or
service.save().subscribe((first) => {
console.log(first); // int e.g. 1000
});
Is there a possibility to get exactly that behaviour?
Hope someone can help!
EDIT:
In my specific use case obs1<int> and obs2<bool> are two different post requests: obs1<int> is the actual save function and obs2<bool> checks if an other service is running.
The value of obs1<int> is needed to reload the page once the request is completed and the value of obs2<bool> is needed to display a message if the service is running - independant of obs1<int>.
So if obs2<bool> emits before obs1<int>, that's not a problem, the message gets display before reload. But if obs1<int> emits before obs2<bool>, the page gets reloaded and the message may not be displayed anymore.
I'm telling this because with the given answers there are different behaviours whether the values get emitted before or after onComplete of the other observable and this can impact the use case.
There are several operators that accomplish this:
CombineLatest
This operator will combine the latest values emitted by both observables, as shown in the marble diagram:
obs1: Observable<int>;
obs2: Observable<Boolean>;
save(): Observable<any> {
return combineLatest(obs1, obs2);
}
save().subscribe((val1, val2) => {
// logic
});
Zip
The Zip operator will wait for both observables to emit values before emitting one.
obs1: Observable<int>;
obs2: Observable<Boolean>;
save(): Observable<any> {
return zip(obs1, obs2);
}
save().subscribe((vals) => {
// Note Vals = [val1, val2]
// Logic
});
Or if you want to use destructuring with the array
save().subscribe(([val1, val2]) => {
// Logic
});
WithLatestFrom
The WithLatestFrom emits the combination of the last values emitted by the observables, note this operator skips any values that do not have a corresponding value from the other observable.
save: obs1.pipe(withLatestFrom(secondSource))
save().subscribe(([val1, val2]) => {
// Logic
});
You can use forkJoin for this purpose. Call them parallely and then if either of them is present then do something.
let numberSource = Rx.Observable.of(100);
let booleanSource = Rx.Observable.of(true);
Rx.Observable.forkJoin(
numberSource,
booleanSource
).subscribe( ([numberResp, booleanResp]) => {
if (numberResp) {
console.log(numberResp);
// do something
} else if (booleanResp) {
console.log(booleanResp);
// do something
}
});
You may use the zip static method instead of concat operator.
save(): Observable<any> {
return zip(obs1, obs2);
}
Then you should be able to do like the following:
service.save().subscribe((x) => {
console.log(x[0]); // int e.g. 1000
console.log(x[1]); // Boolean, e.g. true
});
The exact operator to use depends on the specific details of what you are trying to solve.
A valid option is to use combineLatest - Docs:
obs1$: Observable<int>;
obs2$: Observable<Boolean>;
combined$ = combineLatest(obs1$, obs2$);
combined$.subscribe(([obs1, obs2]) => {
console.log(obs1);
console.log(obs2);
})
Concat emits two events through the stream, one after the other has completed, this is not what you're after.
Merge will emit both events in the same manner, but in the order that they actually end up completing, also not what you're after.
What you want is the value of both items in the same stream event. forkJoin and zip and combineLatest will do this, where you're getting tripped up is that they all emit an array of the values that you're not accessing properly in subscribe.
zip emits every time all items zipped together emit, in sequence, so if observable 1 emits 1,2,3, and observable two emits 4,5; the emissions from zip will be [1,4], [2,5].
combineLatest will emit everytime either emits so you'll get soemthing like [1,4],[2,4],[2,5],[3,5] (depending on the exact emission order).
finally forkJoin only emits one time, once every item inside it has actually completed,a and then completes itself. This is likely what you want more than anything since you seem to be "saving". if either of those example streams don't complete, forkJoin will never emit, but if they both complete after their final value, forkjoin will only give one emission: [2,5]. I prefer this as it is the "safest" operation in that it guarantees all streams are completing properly and not creating memory leaks. And usually when "saving", you only expect one emission, so it is more explicit as well. When ever you see forkJoin, you know you're dealing with a single emission stream.
I would do it like this, personally:
obs1: Observable<int>;
obs2: Observable<Boolean>;
save(): Observable<any> {
return forkJoin(obs1, obs2);
}
service.save().subscribe(([first, second]) => {
console.log(first); // int e.g. 1000
console.log(second); // Boolean, e.g. true
});
Typescript provides syntax like this to access the items in an array of a known length, but there is no way to truly create multiple arguments in a subscribe success function, as it's interface only accepts a single argument.

Error Handling on API Call with FetchJsonP / Redux / React

Hey I am trying to figure out a way to handle error and api call inside an redux epic, I have checked at this doc:
https://redux-observable.js.org/docs/recipes/ErrorHandling.html
I don't have any error, but nothing happens the code seems looping
/**
* Request the JSON guide to the API
* and then dispatch requestGuideFulfilled & requestGameTask actions
*
* #param action$
*/
export function requestGuide(action$) {
return action$.ofType(REQUEST_GUIDE)
.mergeMap(({id}) => fetchJsonp(`${API_URL}/guide/${id}/jsonp`)
.catch(error => requestGuideFailed(error))
)
.mergeMap(response => response.json())
.mergeMap(json => requestGuideFulfilled(json))
.map(json => requestGameTask(json))
}
export function manageRequestGuideError(action$) {
return action$.ofType(REQUEST_GUIDE_FAILED)
.subscribe(({error}) => {
console.log('Error',error)
})
}
Any Idea ? Thank you !
[UPDATE]: I have an error even on the fetching:
You provided an invalid object where a stream was expected. You can
provide an Observable, Promise, Array, or Iterable.
There are a number of issues, so I'll try and elaborate as best I can. To be frank, RxJS is not easy. I would encourage you to spend some solid time learning the fundamentals before using redux-observable, unless of course you're just experimenting in your free time for fun and you like pain lol.
It's also critical to not bring in things like redux-observable unless you really need complex side effect management. Somewhat unfortunately, the docs currently only have simple examples, but redux-observable is really intended to make complex stuff like multiplex websockets, elaborate time-based sequencing, etc much easier at the expense of needing to know RxJS really well. So I guess I'm saying is, if you do need redux, make sure you need redux-observable too or could get away with redux-thunk. It might seem funny that one of the makers of redux-observable sorta talks people out of using it, but I just see a crazy number of people using things like redux-observable/redux-saga for things that simply don't justify the complexity they bring. You know your needs best though, so don't take this as doctrine or be unreasonably discouraged <3
None of the code in this answer has been tested, so it may need minor corrections.
You provided an invalid object where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.
This error is likely caused by .mergeMap(json => requestGuideFulfilled(json)). It looks like requestGuideFulfilled is an action creator, but the source isn't included here so I can't be sure. mergeMap aka flatMap expects you to return another stream (usually an Observable), so an action POJO would need to be wrapped in an Observable, like Observable.of(requestGuideFulfilled(json)) but in this case using mergeMap is unnecessary. It could have just been a regular map().
export function manageRequestGuideError(action$) {
return action$.ofType(REQUEST_GUIDE_FAILED)
.subscribe(({error}) => {
console.log('Error',error)
})
}
In redux-observable all Epics must return an Observable. This Epic is returning a subscription observable (the return value of subscribe()). This actually does produce an error, however due to a bug in RxJS it's been silently swallowed.
Instead you can use do and ignoreElements to create an Observable that listens for that action, logs that property out, then ignores it (never emitting anything itself). So it's "read only" basically.
export function manageRequestGuideError(action$) {
return action$.ofType(REQUEST_GUIDE_FAILED)
.do(({error}) => {
console.log('Error',error)
})
.ignoreElements();
}
The next and biggest issue is where you've placed your catch. It's important to learn about how using RxJS means we are chaining Observables together--"operators" basically take a source and return another Observable which will lazily apply some operation on data piped through them. This is very similar to functional programming with Arrays (e.g. arr.map().filter()) with two major differences: Observables are lazy and they have a time-dimension.
How operators work
So consider this Observable chain:
Observable.of(1, 2, 3)
.map(num => num.toString())
.filter(str => str !== '2');
.subscribe(value => console.log(value));
We create an Observable that, when subscribed to, will synchronously emit 1, then 2, then 3.
We apply the map operator to that source which creates a new Observable that, when subscribed to, will itself subscribe to source we applied it to: our Observable of 1, 2, 3.
We then apply the filter operatorto the Observable returned bymap. As you might have guessed,filterreturns a new Observable that, when subscribed to, will itself subscribe to the source we applied it to: our Observable of strings we mapped. Because thatmap` Observable itself was applied to a source, it too will then subscribe to its source, pulling in the first number and kicking off the map -> filter operation.
It may be helpful to store those intermediate Observables as variables, to demystify things a bit.
const source1: Observable<number> = Observable.of(1, 2, 3);
const source2: Observable<string> = source1.map(num => num.toString());
const result: Observable<string> = source2.filter(str => str !== '2');
Inner Observables
When we use operators like mergeMap, switchMap, concatMap, we are saying we want to map each value to another "inner" Observable who's values will be either merged, switched to, or queued (concat) after the previous inner Observable. These have different important differences, but there's lots of resources on them if you're unfamiliar.
In your case we're using mergeMap, which also has an alias of flatMap, which is the more widely known term used in functional programming. mergeMap will provide each value to your projection function and concurrently subscribe to the Observable you return for each. The values of each of those inner Observables are merged together.
In functional programming they more often call this flattening, instead of merging. So it may again be helpful to first consider this merging/flattening in the context of Arrays
Array.prototype.map
[1, 3, 5].map(value => {
return [value, value + 1];
});
// [[1, 2], [3, 4], [5, 6]] Array<Array<number>>
This resulted in an array of arrays of numbers Array<Array<number>>, but what if we instead wanted a single, flattened array? Enter flatMap.
Array.prototype.flatMap (stage 2 TC39 proposal)
[1, 3, 5].flatMap(value => {
return [value, value + 1];
});
// [1, 2, 3, 4, 5, 6] Array<number>
JavaScript arrays do yet officially have flatMap, but it's a stage 2 TC39 proposal as of this writing. It follows the same semantics as the typical flatMap: for each item in the array map it to another array provided by your projection function, then flatten each of those into a single new array.
With Observables, it's pretty much the same except again they are lazy and have a time dimension:
Observable.of(1, 3, 5).map(value => {
return Observable.of(value, value + 1);
});
// Observable.of(1, 2)..Observable.of(3, 4)..Observable.of(5, 6) | Observable<Observable<number>>
We mapped each number into their own Observable of two numbers. So a higher-order Observable Observable<Observable<number>> and probably not what we wanted in most cases.
Observable.of(1, 3, 5).flatMap(value => {
return Observable.of(value, value + 1);
});
// 1..2..3..4..5..6 | Observable<number>
Now we just have a stream of all the numbers. Perfect!
Error Handling
Putting together our understanding of operating chaining and Observable flattening, we come to error handling. Hopefully that primer makes this next part easier to grok.
If an error is thrown in any one of our chained Observables it will propagate through the chain in the same fashion as values do, but in its own "channel" basically. So we if we have an Observable chain a -> b -> c and an error occurs in a, it will be sent to b then c. When each Observable receives the error it can either handle it in some way, or choose to pass it along to whatever is subscribing to it. When it does, that subscription terminates and no longer listens for future messages from its source.
Most operators just pass along errors (while terminating), so if you aren't using a special error handling operator like catch the error propagates until it reaches your observer--the one you yourself passed to .subscribe(next, error, complete). If you provided that error handler, it's called, if not, it's rethrown as a normal JavaScript exception.
To finally get to your code, let's start with the end; what I think you actually want:
function getGuide(id) {
const promise = fetchJsonp(`${API_URL}/guide/${id}/jsonp`)
.then(res => res.json());
return Observable.from(promise);
}
export function requestGuide(action$) {
return action$.ofType(REQUEST_GUIDE)
.mergeMap(({id}) =>
getGuide(id)
.mergeMap(json => Observable.of(
requestGuideFulfilled(json),
requestGameTask(json)
))
.catch(error => Observable.of(
requestGuideFailed(error)
))
)
}
Now let's break it down.
Promise vs. Observable
First thing you'll see is that I abstracted out your fetchJsonp into the getGuide function. You could just as well put this code inside the epic, but having it separate will make it easier for you to mock it if you decide to test.
As quickly as possible I wrap that Promise in an Observable. Mostly because if we're choosing to use RxJS we should go all-in, especially to prevent confusion later. e.g. both Promise and Observable instances have catch methods so it's easy to cause bugs if you start mixing the two.
Ideally we'd use Observables instead of Promises entirely, as Promises cannot be cancelled (so you cannot cancel the actual AJAX request + JSON parsing itself), although if you wrap it in an Observable and unsubscribe before the promise resolves, the Observable will correctly just ignore what the promise later resolves or rejects.
Emitting multiple actions?
It's not 100% clear, but it appeared you intended to emit two actions in response to successfully getting back the JSON. Your previous code actually maps the JSON to the requestGuideFulfilled() action, but then the next operator maps that action to requestGameTask() (which doesn't receive the JSON, it receives the requestGuideFulfilled() action). Remember above, about how operators are chains of Observables, the values flow through them.
To solve this, we need to think think "in streams". Our getGuide() Observable will emit a single value, the JSON. Given that single (1) value we want to map it to more than one other values, in this case two actions. So we want to transform one-to-many. We need to use one of mergeMap, switchMap, or concatMap then. In this case, since our getGuide() will never emit more than one item all three of these operators will have the same result, but it's critical to understand them cause it often does matter so keep that in mind! Let's just use mergeMap in this case.
.mergeMap(json => Observable.of(
requestGuideFulfilled(json),
requestGameTask(json)
))
Observable.of supports an arbitrary number of arguments, and will emit each of them sequentially.
catching errors
Because our Observable chains are...well..chains hehe the values are piped between them. As we learned above, because of this where you place error handling in these chains is important. This is actually not very different between error handling with traditional exceptions, or even promises, but Promises do not have "operators", so people don't usually run into this confusion.
The catch operator is the most common, and it's pretty similar to the catch Promises have except you must return an Observable of the value you want, not the value itself. Observable.of is common here since most often we just want to emit one or more items sequentially.
.catch(error => Observable.of(
requestGuideFailed(error)
))
Whenever a error is emitted by the source we apply this operator to, it will catch it and instead emit requestGuideFailed(error) and then complete().
Because it emits an action on error, any operators we apply to the result of this .catch() **could also be operating on the value our catch emits.
getJsonSomehow()
.catch(error => Observable.of(
someErrorAction(error)
))
.map(json => {
// might be the JSON, but also might be the
// someErrorAction() action!
return someSuccessAction();
})
Although not unique to redux-observable (since redux-observable is mostly just a tiny library and a convention, using RxJS) you'll often see Epics follow a similar pattern.
Listens for an particular action
Then merges or switches that action into an inner Observable that performs a side effect
When that side effects is successful we map it to an success action
In case it errors we place a catch inside our mergeMap/switchMap but most often at the end of the inner chain, so that any actions we emit aren't transformed on accident.
You'll hopefully recognize that general pattern from the redux-observable docs:
function exampleEpic(action$) {
return action$.ofType(EXAMPLE)
.mergeMap(action =>
getExample(action.id)
.map(resp => exampleSuccess(resp))
.catch(resp => Observable.of(
exampleFailure(resp)
))
);
}
Applying that knowledge to our previous work:
getGuide(id)
.mergeMap(json => Observable.of(
requestGuideFulfilled(json),
requestGameTask(json)
))
.catch(error => Observable.of(
requestGuideFailed(error)
))
And that's about it, I think.
PHEW! Sorry that was so long-winded. It's entirely possible you knew some or all of this, so forgive me if I'm preaching the choir! I started writing something short, but kept adding clarification, after clarification. lol.
If you're struggling, definitely make sure using RxJS and redux-observable (or any complex middleware) is a neccesary complexity for your app.

Categories

Resources