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
Probably one of the first things anyone starting out on Redux wonders is, can you dispatch multiple actions? The answer of course is, yes, just either call dispatch multiple times or iterate dispatch over an array.
Using an async middleware like Redux Thunk certainly enables scenarios such as dispatching multiple distinct but related actions in a row, dispatching actions to represent progression of an AJAX request, dispatching actions conditionally based on state, or even dispatching an action and checking the updated state ...
redux-observable is great for global event handling because we have access to the global state. Here, we're able to keep our code naturally DRY because our FetchData action and our key event both trigger the UpdateData action.
Epics are the core element of the redux middleware called redux-observable. redux-observable is based on RxJS. RxJS is a library for reactive programming using Observables, to make it easier to compose asynchronous or callback-based code. stream The concept of stream is "sequence of data made available over time.".
For RxJs6:
action$.pipe(
concatMap(a => of(Action1, Action2, Action3))
)
Note that we have concatMap
, mergeMap
and switchMap
that could do the job, the differences: https://www.learnrxjs.io/operators/transformation/mergemap.html
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.
Why do you use Observable.concat? SwitchMap waits for Observable or Promise which contains value (array of actions in our case) from its callback function. So no need to return Observable in returned promise success handler. try this one:
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 },
{ type: GET_CATEGORIES_REQUEST }
])
})
.catch(err => {
return { type: ADD_CATEGORY_ERROR, payload: err }
})
})
}
You can also try using store inside your epics
const addCategoryEpic = (action$, store) => {
return action$.ofType(ADD_CATEGORY_REQUEST)
.switchMap((action) => {
const db = firebase.firestore()
const user = firebase.auth().currentUser
const query = db.collection('categories')
.doc()
.set({
name: action.payload.name,
uid: user.uid
})
return Observable.fromPromise(query)
}).map((result) => {
store.dispatch({ type: ADD_CATEGORY_SUCCESS });
return ({ type: GET_CATEGORIES_REQUEST })
})
.catch(err => {
return { type: ADD_CATEGORY_ERROR, payload: err }
})
}
I found that I should use create an observable using Observable.fromPromise
then use flatMap
to make multiple actions
const addCategoryEpic = action$ => {
return action$.ofType(ADD_CATEGORY_REQUEST)
.switchMap((action) => {
const db = firebase.firestore()
const user = firebase.auth().currentUser
const query = db.collection('categories')
.doc()
.set({
name: action.payload.name,
uid: user.uid
})
return Observable.fromPromise(query)
.flatMap(() => ([
{ type: ADD_CATEGORY_SUCCESS },
{ type: GET_CATEGORIES_REQUEST }
]))
.catch(err => {
return { type: ADD_CATEGORY_ERROR, payload: err }
})
})
}
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