I want to dispatch multiple actions from a redux-observable epic. How can I do it? I originally started with
const addCategoryEpic = action$ => {
return action$.ofType(ADD_CATEGORY_REQUEST)
.switchMap((action) => {
const db = firebase.firestore()
const user = firebase.auth().currentUser
return db.collection('categories')
.doc()
.set({
name: action.payload.name,
uid: user.uid
})
.then(() => {
return { type: ADD_CATEGORY_SUCCESS }
})
.catch(err => {
return { type: ADD_CATEGORY_ERROR, payload: err }
})
})
}
Now instead of just dispatching ADD_CATEGORY_SUCCESS
, I also want to refresh the listing (GET_CATEGORIES_REQUEST
). I tried many things but always get
Actions must be plain objects. Use custom middleware for async actions
For example:
const addCategoryEpic = action$ => {
return action$.ofType(ADD_CATEGORY_REQUEST)
.switchMap((action) => {
const db = firebase.firestore()
const user = firebase.auth().currentUser
return db.collection('categories')
.doc()
.set({
name: action.payload.name,
uid: user.uid
})
.then(() => {
return Observable.concat([
{ type: ADD_CATEGORY_SUCCESS },
{ type: GET_CATEGORIES_REQUEST }
])
})
.catch(err => {
return { type: ADD_CATEGORY_ERROR, payload: err }
})
})
}
Or changing switchMap
to mergeMap
etc
The issue is that inside your switchMap
you're returning a Promise which itself resolves to a concat Observable; Promise<ConcatObservable>
. the switchMap
operator will listen to the Promise and then emit the ConcatObservable as-is, which will then be provided to store.dispatch
under the hood by redux-observable. rootEpic(action$, store).subscribe(store.dispatch)
. Since dispatching an Observable doesn't make sense, that's why you get the error that actions must be plain objects.
What your epic emits must always be plain old JavaScript action objects i.e. { type: string }
(unless you have additional middleware to handle other things)
Since Promises only ever emit a single value, we can't use them to emit two actions, we need to use Observables. So let's first convert our Promise to an Observable that we can work with:
const response = db.collection('categories')
.doc()
.set({
name: action.payload.name,
uid: user.uid
})
// wrap the Promise as an Observable
const result = Observable.from(response)
Now we need to map that Observable, which will emit a single value, into multiple actions. The map
operator does not do one-to-many, instead we'll want to use one of mergeMap
, switchMap
, concatMap
, or exhaustMap
. In this very specific case, which one we choose doesn't matter because the Observable we're applying it to (that wraps the Promise) will only ever emit a single value and then complete(). That said, it's critical to understand the difference between these operators, so definitely take some time to research them.
I'm going to use mergeMap
(again, it doesn't matter in this specific case). Since mergeMap
expects us to return a "stream" (an Observable, Promise, iterator, or array) I'm going to use Observable.of
to create an Observable of the two actions we want to emit.
Observable.from(response)
.mergeMap(() => Observable.of(
{ type: ADD_CATEGORY_SUCCESS },
{ type: GET_CATEGORIES_REQUEST }
))
These two actions will be emitted synchronously and sequentially in the order I provided them.
We need to add back error handling too, so we'll use the catch
operator from RxJS--the differences between it and the catch
method on a Promise are important, but outside the scope of this question.
Observable.from(response)
.mergeMap(() => Observable.of(
{ type: ADD_CATEGORY_SUCCESS },
{ type: GET_CATEGORIES_REQUEST }
))
.catch(err => Observable.of(
{ type: ADD_CATEGORY_ERROR, payload: err }
))
Put it all together and we'll have something like this:
const addCategoryEpic = action$ => {
return action$.ofType(ADD_CATEGORY_REQUEST)
.switchMap((action) => {
const db = firebase.firestore()
const user = firebase.auth().currentUser
const response = db.collection('categories')
.doc()
.set({
name: action.payload.name,
uid: user.uid
})
return Observable.from(response)
.mergeMap(() => Observable.of(
{ type: ADD_CATEGORY_SUCCESS },
{ type: GET_CATEGORIES_REQUEST }
))
.catch(err => Observable.of(
{ type: ADD_CATEGORY_ERROR, payload: err }
))
})
}
While this works and answers your question, multiple reducers can make state changes from the same single action, which is most often what one should do instead. Emitting two actions sequentially is usually an anti-pattern.
That said, as is common in programming this is not an absolute rule. There definitely are times where it makes more sense to have separate actions, but they are the exception. You're better positioned to know whether this is one of those exceptional cases or not. Just keep it in mind.