niba
niba

Reputation: 2911

How to chain async actions and wait for the result without store.dispatch

I'm trying to write my INITIALIZE action which should chain some async actions together in the following way

  1. Call the initialize action.
  2. Call two async actions simultaneously.
  3. Wait for the completion of above actions.
  4. Run additional one action.
  5. Finish initialization.

here is the redux flow that I expect

INITIALIZATION_STARTED => ASYNC_ACTION_A_STARTED AND ASYNC_ACTION_B_STARTED => ASYNC_ACTION_A_FINISHED AND ASYNC_ACTION_B_FINISHED => ASYNC_ACTION_C_STARTED => ASYNC_ACTION_C_FINISHED => INITIALIZATION_FINISHED

I managed to achieve that flow using store.dispatch inside my epic, I know that this is anti-pattern and it will be removed in the 1.0.0 version so I would like to know how I can do it using pure epics

My working solution

export const initEpic = (action$: ActionsObservable<Action>, store) =>
  action$.filter(actions.initialization.started.match)
    .switchMap(action => (
      Observable.forkJoin(
        waitForActions(action$, actions.asyncA.done, actions.asyncB.done),
        Observable.of(
          store.dispatch(actions.asyncA.started(action.payload)),
          store.dispatch(actions.asyncB.started(action.payload)),
        )
      ).map(() => actions.asyncC.started(action.payload))
    )
  );

const waitForActions = (action$, ...reduxActions) => {
  const actionTypes = reduxActions.map(x => x.type);
  const obs = actionTypes.map(type => action$.ofType(type).take(1));
  return Observable.forkJoin(obs);
}

I have also been trying to use forkEpic from this comment like that

export const initEpic = (action$: ActionsObservable<Action>, store) =>
  action$.filter(actions.initialization.started.match)).mergeMap(action =>
    forkEpic(loadTagsEpic, store, actions.asyncA.started(action.payload))
      .concat(
        forkEpic(loadBranchesEpic, store, actions.asyncB.started(action.payload))
      )
      .map(() => actions.asyncC.started(action.payload))
  );

but it doesn't dispatch starting actions ASYNC_ACTION_A_STARTED and _ASYNC_ACTION_B_STARTED

Upvotes: 0

Views: 877

Answers (1)

jayphelps
jayphelps

Reputation: 15401

Sounds like merge is perfect for this. You'll start listening for asyncA.done and asyncB.done and then while waiting you'll kick off the requests by emitting asyncA.started and asyncB.started. These two streams are merged together as one, so it happens in the correct order and the actions emitted by either are emitted by our epic without needing store.dispatch.

const initEpic = action$ =>
  action$.filter(actions.initialization.started.match)
    .switchMap(action => (
      Observable.merge(
        waitForActions(action$, actions.asyncA.done, actions.asyncB.done)
          .map(() => actions.asyncC.started(action.payload)),
        Observable.of(
          actions.asyncA.started(action.payload),
          actions.asyncB.started(action.payload),
        )
      )
    )
  );

Here is a JSBin demoing: https://jsbin.com/yonohop/edit?js,console

It doesn't do any of the ASYNC_ACTION_C_FINISHED and INITIALIZATION_FINISHED stuff because code for that was not included in the question so not sure what it would have done. 😁

You might notice this is mostly a regular RxJS question where the items streaming happen to be actions. This is really helpful because when you ask for help you can ask from the entire RxJS community if you craft the question as generic RxJS.


Note that I listened for done before starting; this is generally a best practice in case done is emitted synchronously after started. If you didn't listen first, you'd miss it. Since it's async it doesn't matter, but still generally a best practice and helpful when you unit test.

Upvotes: 1

Related Questions