Rody Kirwan
Rody Kirwan

Reputation: 153

In redux-observable - How can I dispatch an action "mid-stream" without returning the value of that action?

I have searched everywhere and cannot find an answer to the following...

In my addItemEpic I would like to dispatch a "loading" action before sending the ajax request - I DO NOT want the value of the action to be returned - I just want to DO the action then return the ajax response. This can be achieved easily with store.dispatch() as below:

export const addItemsEpic = (action$, store) =>
  action$.ofType(ADD_ITEM)
    .do(() => store.dispatch({
        type: 'layout/UPDATE_LAYOUT',
        payload: { loading: true } 
    }))
    .switchMap(action => api.addItem(action.payload))
    .map(item => loadItem(item))
    .do(() => store.dispatch({
       type: 'layout/UPDATE_LAYOUT',
       payload: { loading: false } 
    }))
    .catch(error => Observable.of({
       type: 'AJAX_ERROR',
       payload: error,
       error: true
   }));

However store.dispatch() is deprecated and discouraged in redux-observable.

I have attempted to use almost every plausible operator and still I cannot dispatch the action without disrupting the return value in the next chained function. I thought something like te following should do it:

action$.ofType(ADD_ITEM)
   .concatMap(action => Observable.of(
      updateLayout({loading: true}),
      api.addItem(action.payload)).last())
   .map(item => loadItem(item))

But unfortunately 2 problems here:

I'd really appreciate some help or advice here as I can't find any way to get this working.

Thanks in advance

Upvotes: 8

Views: 2286

Answers (2)

jayphelps
jayphelps

Reputation: 15401

Robert's answer is great and the correct one. Here are a few extra thoughts I had:

Reuse existing actions

Very often when you find yourself synchronously following one action after another this is a sign that your reducer should only need the first one.

e.g. instead of having layout/UPDATE_LAYOUT to set loading: true, your reducer could just know that whenever there's an ADD_ITEM and LOAD_ITEM that means it should be loading: true. If that implicitness isn't your bag, your could instead use extra metadata as the signal rather than the action type itself.

const layoutReducer = (state = { loading: false }, action) => {
  // instead of listening for an action type, we're looking for some other 
  // metadata by convention. In this case the `layout` property.
  if (action.layout) {
    return { ...state, ...action.layout };
  } else {
    return state;
  }
};

// the schema you use (payload vs layout vs metadata, etc) is your call
store.dispatch({
  type: 'DOESNT_MATTER',
  payload: 'whatever',
  layout: { loading: true }
});

All that said, your example may have been simplified to make it easier to ask on SO (a great idea). Sometimes it does indeed make sense to have them separate actions for clarity, separation of concerns, reusablity, or simply because you have to because you don't control the library that is listening for it. e.g. react-router-redux you need to dispatch special actions to do route transitions.

So think of this just as "keep it in mind" advice rather than doctrine :)

concat

concat supports any number of arguments, so you only need one of them:

const addItemEpic = action$ => action$
  .ofType(ADD_ITEM)
  .switchMap(action => {
    const apiCall$ = Observable.fromPromise(api.addItem(action.payload))
      .map(item => actions.loadItem(item))
      .catch(err => Observable.of(actions.ajaxError(err)));

    return Observable.concat(
      Observable.of(actions.updateLayout(true)),
      apiCall$,
      Observable.of(actions.updateLayout(false))
    );
  });

// or 

const addItemEpic = action$ => action$
  .ofType(ADD_ITEM)
  .switchMap(action =>
    Observable.concat(
      Observable.of(actions.updateLayout(true))
      Observable.fromPromise(api.addItem(action.payload))
        .map(item => actions.loadItem(item))
        .catch(err => Observable.of(actions.ajaxError(err))),
      Observable.of(actions.updateLayout(false))
    )
  );

Upvotes: 4

Robert Farley
Robert Farley

Reputation: 2456

The second form is on the right path. The idea is to return an Observable that first emits the loading action, then after the service call, emits either the loaded action or error action, and finally emits the done loading action.

const addItemEpic = action$ => action$
  .ofType(ADD_ITEM)
  .switchMap(action => { // or use mergeMap/concatMap depending on your needs
    const apiCall$ = Observable.fromPromise(api.addItem(action.payload))
      .map(item => actions.loadItem(item))
      .catch(err => Observable.of(actions.ajaxError(err)));

    return Observable.concat(
      Observable.of(actions.updateLayout(true)),
      Observable.concat(apiCall$,
        Observable.of(actions.updateLayout(false))));
  });

Observable.fromPromise() converts a Promise to an Observable

Upvotes: 5

Related Questions