1
votes

This is a problem that we encountered in our application after we upgraded to version 1.x, where they introduced queuing actions dispatched epic to be executed after the current stack is complete (https://github.com/redux-observable/redux-observable/pull/493).

Imagine an action (INIT_STUFF), that has an epic that returns a list of actions (INIT_A, INIT_B, in that order). Each of these actions have an epic of their own. They do some stuff there and return an action that modifies the store.

In previous version of redux-observable, the epic for INIT_B could rely on the fact that the epic for INIT_A has completed executing and has made changes to the store. This epic could then use the updated store.

In the latest version, both the epics for INIT_A and INIT_B are executed in their respective order, but the actions they have returned (that modify the store) are deferred until the completion of the last epic. This means that the epic for INIT_B does not have access to the updates in the store made by INIT_A.

Here's a simple implementation of what I am talking about: https://redux-observable-playground-ip3qfb.stackblitz.io

What could be the migration path for a use case like this?

1
Thanks for the stackblitz! It tries to log store$.initialized however as far as I can tell initialized is never a property in your redux state. It's also unclear from the demo what the intended behavior is? I don't see any two epics relying on each other. Separately, your description I think makes sense to me, but I'd like to understand the stackblitz demo to confirm.jayphelps
Oops I think I get the demo now. initialized is an object because it's the name of one of the reducers provided to combineReducers.jayphelps
@jayphelps I updated it to better reflect what I meant.squgeim

1 Answers

1
votes

This one is indeed tricky. Personally, I think having two epics need to rely on things like this would be an anti-pattern, particularly because they're both synchronous. I assume that isn't just because it's contrived because otherwise you wouldn't face this problem I think.

If Epic B truly should wait to begin until Epic A is ready, either the parent (initializeStuffEpic) should make sure this sequence is enforced, or Epic B could listen for some signal to know A is ready because Epics are "not supposed to" leak their implementation details like this. Alternatively, this might be a sign that the logic should not be two separate Epics but instead just one, using function composition to keep things organized.

Epics usually respond to actions as their signals, though the second argument, state$, is also an Observable and can be subscribed to as well. If you really prefer to do things this way, I think this might be the easiest way:

https://stackblitz.com/edit/redux-observable-playground-g1nuis?file=initialize.js

export const initBEpic = (action$, state$) =>
  action$.pipe(
    ofType("INITIALIZE_B"),
    mergeMap(({ payload }) => {
      return state$.pipe(
        filter(state => state.initialized.A), // Wait until it is indeed true
        take(1), // Very important!
        map(() => ({
          type: "IS_B_READY",
          payload: true
        }))
      );
    })
  );

As with most things, there are caveats. What if A is never ready?

--

The change made to how actions are scheduled in redux-observable v1.0 was meant to make some things more intuitive, while discouraging some other bad patterns. This particular pattern was not considered during that decision, so I'm not entirely sure if I would have changed the behavior to make your use case easier, or not. While it's unfortunately something that might seem obvious doesn't work, it likely is an acceptable tradeoff as this is the first I've seen this.

It's helpful to think of Epics almost as separate processes (though they aren't.) It wouldn't be safe to assume the timing of shared state implicitly, without an actual signal (an action or state$ update.)

There's always a chance we could change it for v2, however someone would need to make a proposal or PR for the specific changes, and how scheduling works--an internal instance of QueueScheduler--is a bit complicated.