Reputation: 46467
I'm struggling with redux-observable
, trying to figure out how to create an epic with this flow:
GET_ITEMS_REQUEST
actionGET_ITEM_DETAILS_REQUEST
action, which will send another HTTP request to get some more details for those itemsGET_ITEMS_SUCCESS
action, which will update the redux stateGoing from step 3 to 4 is where I'm stuck. I know how to dispatch GET_ITEM_DETAILS_REQUEST
with the item IDs, but I don't know how to listen/subscribe to the GET_ITEM_DETAILS_REQUEST
action to get the item details response.
So far, I have the following:
function getItemsEpic(action$) {
return action$
// step 1
.ofType('GET_ITEMS_REQUEST')
.mergeMap(() => {
// step 2
return Observable.from(Api.getItems())
})
.mergeMap((items) => {
// step 3
const itemIds = items.map((item) => item.id);
return Observable.of({
type: 'GET_ITEM_DETAILS_REQUEST',
ids: itemIds
});
})
// ... what now?
.catch(() => {
return Observable.of({
type: 'GET_ITEMS_FAILURE'
});
});
}
Upvotes: 2
Views: 499
Reputation: 15401
One approach is, after you received the items, start listening for GET_ITEM_DETAILS_FULFILLED
and then immediately kick off GET_ITEM_DETAILS_REQUEST
using startWith()
. The other epic will look the details up and emit GET_ITEM_DETAILS_FULFILLED
which our other epic is patiently waiting for and then will zip the two (items + details) together.
const getItemDetailsEpic = action$ =>
action$
.ofType('GET_ITEM_DETAILS_REQUEST')
.mergeMap(({ ids }) =>
Observable.from(Api.getItemDetails(ids))
.map(details => ({
type: 'GET_ITEM_DETAILS_FULFILLED',
details
}))
);
const getItemsEpic = action$ =>
action$
.ofType('GET_ITEMS_REQUEST')
.mergeMap(() =>
Observable.from(Api.getItems())
.mergeMap(items =>
action$.ofType('GET_ITEM_DETAILS_FULFILLED')
.take(1) // don't listen forever! IMPORTANT!
.map(({ details }) => ({
type: 'GET_ITEMS_SUCCESS',
items: items.map((item, i) => ({
...item,
detail: details[i]
// or the more "safe" `details.find(detail => detail.id === item.id)`
// if your data structure allows. Might not be necessary if the
// order is guaranteed to be the same
}))
}))
.startWith({
type: 'GET_ITEM_DETAILS_REQUEST',
ids: items.map(item => item.id)
})
)
);
Separately, I noticed you put your catch()
on the outer Observable chain. This is probably not going to do entirely what you want. By the time the error reaches the top chain, your entire epic will have been terminated--it will no longer be listening for future GET_ITEMS_REQUEST
! This is a very important distinction and we often call it "isolating your Observable chains". You don't want errors to propagate further than they should.
// GOOD
const somethingEpic = action$ =>
action$.ofType('SOMETHING')
.mergeMap(() =>
somethingThatMayFail()
.catch(e => Observable.of({
type: 'STUFF_BROKE_YO',
payload: e,
error: true
}))
);
// NOT THE SAME THING!
const somethingEpic = action$ =>
action$.ofType('SOMETHING')
.mergeMap(() =>
somethingThatMayFail()
)
.catch(e => Observable.of({
type: 'STUFF_BROKE_YO',
payload: e,
error: true
}));
You sometimes do want a catch on the outer chain, but that's a last ditch thing usually only for errors that are not recoverable.
Upvotes: 1