Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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:

  • no loading-action is dispatched (the value IS returned as an observable)
  • api request now returns a wrapped promise instead of a mappable value

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

Thanks in advance

like image 439
Rody Kirwan Avatar asked Dec 08 '17 12:12

Rody Kirwan


2 Answers

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

like image 85
Robert Farley Avatar answered Sep 28 '22 05:09

Robert Farley


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))
    )
  );
like image 45
jayphelps Avatar answered Sep 28 '22 03:09

jayphelps