I've set up an epic that waits for another epic to complete, much like @jayphelps' answer here: Invoking epics from within other epics
However I've found that it only seems to run once. After that I can see the CART_CONFIG_READY
action in the console but the DO_THE_NEXT_THING
action is not triggered.
I've tried various combinations of mergeMap
and switchMap
, with and without take
but nothing seems to help.
This is (kind of) what my code looks like.
import { NgRedux } from '@angular-redux/store';
import { Observable } from 'rxjs/Observable';
import { ActionsObservable } from 'redux-observable';
export class CartEpicsService {
checkCart = (action$: ActionsObservable<any>, store: NgRedux<any>) => {
return action$.ofType('CHECK_CART')
.switchMap(() => {
console.log('___LISTENING___');
return action$.ofType('CART_CONFIG_READY')
.take(1) // removing this doesn't help
.mergeMap(() => {
console.log('___RECEIVED___');
// do stuff here
return Observable.of({
type: 'DO_THE_NEXT_THING'
});
})
.startWith({
type: 'GET_CART_CONFIG'
});
});
}
getCartConfig = (action$: ActionsObservable<any>, store: NgRedux<any>) => {
return action$.ofType('GET_CART_CONFIG')
.switchMap(() => {
const config = store.getState().config;
// we already have the config
if (config) {
return Observable.of({
type: 'CART_CONFIG_READY'
});
}
// otherwise load it from the server using out HTTP service
return this.http.get('/cart/config')
.switchMap((response) => {
return Observable.concat(
Observable.of({
type: 'CART_CONFIG_SUCCESS'
}),
Observable.of({
type: 'CART_CONFIG_READY'
})
);
})
.catch(error => Observable.of({
type: 'CART_CONFIG_ERROR',
error
}));
});
}
}
For context I need the response from the /cart/config endpoint to check the validity of the cart. I only need to download the config once.
Here is a runnable example on JS Bin:
Dang this is definitely a tricky one!
Cause
When
state.config === true
you return an Observable ofCART_CONFIG_READY
that emits synchronously, whereas during the first time the http request (or delay, in the jsbin) means it is always going to be async.Why this makes a difference is in the
checkCart
epic you return an observable chain that listens forCART_CONFIG_READY
withaction$.ofType('CART_CONFIG_READY')
but also applies a.startWith({ type: 'GET_CART_CONFIG' })
. That means thatGET_CART_CONFIG
is going to be emitted synconously beforeaction$.ofType('CART_CONFIG_READY')
is subscribed becausestartWith
is basically shorthand for a concat, which might would make the issue more clear if you're familiar with it. It's nearly exactly the same as doing this:So to summarize, what is happening the second time around
GET_CART_CONFIG
is dispatched synchronously,getCartConfig
receives it and sees the config is already in the store so it synchronously dispatchesCART_CONFIG_READY
. But we are not yet listening for it incheckCart
so it goes unanswered. Then that callstack returns and the next Observable in the concat, ouraction$.ofType('CART_CONFIG_READY')
chain, gets subscribed to. But too late, the action it listens for has already been emitted!Solutions
One way to fix this is to make either the emitting of
CART_CONFIG_READY
always async, or to start listening for it in the other epic before we dispatchGET_CART_CONFIG
.1. emit CART_CONFIG_READY async
Observable.of
accepts a scheduler as its last argument, and RxJS supports several of them.In this case you could use the
AsyncScheduler
(macrotask) or theAsapScheduler
(microtask). Both will work in this case, but they schedule on different times in the JavaScript event loop. If you're not familiar with event loop tasks, check this out.I would personally recommend using the
AsyncSheduler
in this case because it will provide the closest async behavior to making an http request.2. Listen for
CART_CONFIG_READY
before emittingGET_CART_CONFIG
Because
startWith
is shorthand for aconcat
(which we don't want to do) we instead need to use some form ofmerge
, with ourofType
chain first so that we listen before emitting.You only need to do one of these solutions, but it wouldn't hurt to do both of them. Offhand I would probably recommend using both just so that things are consistent and expected, even if they are a bit more verbose.
You might also be happy to know that
Observable.of
accepts any number of items, which will be emitted in order. So you don't need to useconcat
:Thanks so much for the jsbin btw, it made it much easier to debug.
Edit based on your comment:
A combination of both. I've dealt with a ton of async/scheduled code and ordering is very commonly the source of issues. I scanned the code, mentally picturing execution, noticed the difference in async vs sync depending on codepath, then I made a quick operator to make it easy for me to confirm the order in which any Observable chain is subscribed to.
I applied it to several places, but the most important are these two:
It confirmed that in the second code path
CART_CONFIG_READY
was getting emitted before the other epic was listening for it.