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
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
Robert's answer is great and the correct one. Here are a few extra thoughts I had:
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 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))
)
);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With