0

The following code works without errors:

export const myEpic = (action$: any) => action$.pipe(
   ofType("TEST"),
   mergeMap(() => concat(
      // fires an actionCreator and triggers another epic
      of(actionOne()),
      // fires an actionCreator
      of(actionTwo())
   ))
);

The problem is that I need the data from actionOne to be available before actionTwo gets fired, and it doesn't seem to be happening. So I want to make this an async function like:

export const myEpic = (action$: any) => action$.pipe(
   ofType("TEST"),
   mergeMap(async () => concat(
      of(await actionOne()),
      of(actionTwo())
   ))
);

This throws an error:

Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.

EDIT

Other relevant code:

// main component that loads
constructor(props) {
   props.dispatch(init());
}
componentDidUpdate(prevProps) {
   if (prevProps.actionTwoFlag !== this.props.actionTwoFlag) {
      // do stuff with result from actionOne
      // error is thrown here because there's no data
   }
}

// actions
export const init = () => ({ type: "TEST" });
export const actionOne = () => ({ type: "ACTION_ONE" });
export const actionOneDone = (result) => ({ type: "ACTION_ONE_DONE", payload: result });
export const actionTwo = () => ({ type: "ACTION_TWO", payload: true });

// epics
export const actionOneEpic = (action$: any) => action$.pipe(
   ofType("ACTION_ONE"),
   mergeMap(() =>
      ajax(..).pipe(
         mergeMap(result => concat(
            of(actionOneDone(result)),
            ...
         ))
      )
   )
);
);
NJB
  • 51
  • 4

1 Answers1

0

There are various ways to solve this.

1- One way is just using defer() operator on the actionTwo. What defer() operator would do, is execute your code on subscription, since they are concatenated, the subscription to of(actionTwo()) would be done after of(actionOne()) is completed:

export const myEpic = (action$: any) => action$.pipe(
   ofType("TEST"),
   mergeMap(() => concat(
      of(actionOne()),
      defer(() => of(actionTwo()))
   ))
);

2- Another option is just do a switchMap(), this would ensure too that when you create the of(actionTwo()) observable, the of(actionOne()) observable has already been emitted and finished. switchMap() also ensures sequential order, so you can safely remove the concat() operator:

    export const myEpic = (action$: any) => action$.pipe(
       ofType("TEST"),
       mergeMap(() => 
          of(actionOne()).pipe(switchMap(() => of(actionTwo())))
       )
    );

EDIT:

Now I think I got it, although, I am not pretty familiar with redux observable epics. I have seen a solution here: Composing and sequencing multiple epics in redux-observable that may solve your issue two. Based on that, I will give 2 proposals.

1st proposal:

This proposal just builds an epic that push action one at first, and waits for action one done in order to push the action two.

export const myEpic = (action$: any) => action$.pipe(
    ofType('TEST'),
    map(() => actionOne()),
    mergeMap(() => {
        return action$.pipe(
            ofType('ACTION_ONE_DONE'),
            take(1),
            map(() => actionTwo()),
        );
    })
);

2nd proposal:

Do it all in one epic. Since both action one and action two are related (one depend on each other) it could make sense to merge both into only one epic, it would be something like this:

export const myEpic = (action$: any) => action$.pipe(
    ofType('TEST'),
    map(() => actionOne()),
    mergeMap(() => {
        return ajax(..).pipe(
            mergeMap((data) => {
                return concat(
                    actionOneDone(action),
                    of(actionTwo()).mergeMap(() => /* Do action two */ actionTwoDone())
                )
            }),
        )
    })
);

Hope this helps!

  • Thanks. I gave both ways a try, but I still get an error. I think it's because the actionOne is an actionCreator, so that finishes quickly and fires actionTwo. But the actionOne triggers an epic, which doesn't finish before actionTwo gets called. I can check for data before actionTwo runs, but was hoping there was a simpler way. – NJB May 07 '19 at 13:52
  • So, If I have understood well, actionOne() triggers some async function that you named 'trigger an epic'? if so, you should wrap all that async functionality into an observable. Also, can you explain a little bit more what is to 'trigger an epic'? And a little bit more the workflow/functionality you are trying to achieve? I am secure this could be done the 'right' way. – Llorenç Pujol Ferriol May 07 '19 at 14:21
  • I added extra code to the description. The component calls the init() function, which fires myEpic. actionOne in myEpic eventually calls another Epic, but that data isn't available once actionTwo runs. – NJB May 07 '19 at 14:33
  • Thanks, I edited to my solution too. Check it out. If this solution still doesn't fit what you need, maybe you could find something interesting in this stackoverflow question: https://stackoverflow.com/questions/48261438/composing-and-sequencing-multiple-epics-in-redux-observable – Llorenç Pujol Ferriol May 09 '19 at 09:37
  • thank you! I think I can get solution 1 to work after some tweaks. – NJB May 09 '19 at 14:04
  • Great! can you share those tweaks? I could put them in the answer so other users can get help from the solution – Llorenç Pujol Ferriol May 09 '19 at 16:04
  • Sorry, I just saw this comment. I thought the tweaks would work, but it didn't. The issue turned out to be because one of the React components updates when the props changes (which it did when the first action runs), so it caused downstream errors when actionTwo isn't run yet. I had to change the component to check if data from actionTwo exists or update the mapStateToProps so actionOne data doesn't trigger a rerender. – NJB Jun 22 '19 at 16:02