Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to dispatch multiple actions from redux-observable?

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

like image 989
Jiew Meng Avatar asked Dec 25 '17 02:12

Jiew Meng


People also ask

Can you dispatch two actions in redux?

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.

Can middleware dispatch multiple actions?

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 ...

Should I use Redux-observable?

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.

What is epics in redux?

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.".


5 Answers

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

like image 191
keos Avatar answered Oct 28 '22 03:10

keos


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.

like image 29
jayphelps Avatar answered Oct 28 '22 03:10

jayphelps


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 }
        })
    })
}
like image 39
Alexander Poshtaruk Avatar answered Oct 28 '22 05:10

Alexander Poshtaruk


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 }
   })
}
like image 20
Denis Rybalka Avatar answered Oct 28 '22 05:10

Denis Rybalka


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 }
        })
    })
}
like image 40
Jiew Meng Avatar answered Oct 28 '22 05:10

Jiew Meng