Hristo
Hristo

Reputation: 46467

using redux-observable epics to decorate data through multiple actions

I'm struggling with redux-observable, trying to figure out how to create an epic with this flow:

  1. listen for a GET_ITEMS_REQUEST action
  2. send an HTTP request to get some items
  3. grab the IDs of those items and dispatch a GET_ITEM_DETAILS_REQUEST action, which will send another HTTP request to get some more details for those items
  4. decorate the items from the first request with the details from the second request and dispatch a final GET_ITEMS_SUCCESS action, which will update the redux state

Going 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

Answers (1)

jayphelps
jayphelps

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

Related Questions