Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Why saga.run() is out of order in the Redux middleware stack? #1651

Closed
compulim opened this issue Oct 26, 2018 · 3 comments
Closed

Why saga.run() is out of order in the Redux middleware stack? #1651

compulim opened this issue Oct 26, 2018 · 3 comments

Comments

@compulim
Copy link

saga.run() is out of order in the Redux middleware stack

https://codesandbox.io/s/lp7on8mjpz

When dispatching an action, the order of the middleware (including saga) in the Redux is not in the original order. For example, I have 2 middleware, and between them, I have a saga middleware. The 2 middleware will always run first, before the saga.

My store enhancer looks like this:

applyMiddleware(
  () => next => action => {
    console.log('1: First custom middleware');

    return next(action);
  },
  sagaMiddleware,
  () => next => action => {
    console.log('3: Second custom middleware');

    return next(action);
  }
)

And my saga looks like this:

sagaMiddleware.run(function* () {
  console.log('2A: redux-saga initialized');

  const taken = take('*');

  console.log('2B: take called');

  yield taken;

  console.log('2C: redux-saga got action');
});

I always see:

2A: redux-saga initialized
2B: take called
1: First custom middleware
3: Second custom middleware
2C: redux-saga got action

I understand why 2A/2B happen that way. But I am not quite sure the primary reason we want to run 2C after 3.

Investigating into redux-saga code, I found that in middleware.js:44, it specifically delayed the run of the saga middleware. It only run after all middleware has completed the job, i.e. next() is not called before processing of the default channel.

const result = next(action) // hit reducers
channel.put(action)
return result

I tried to look into documentations but couldn't find a place that mention this. I think this is by design, but I would like to know why it is designed this way.

Because of this design, it is very difficult to mix-and-match middleware and saga. If there is an action that need to be handled by saga before the middleware, it will be impossible without splitting it into 2 actions. That also means, if I need to migrate my app from middleware to saga progressively, there will be a chance that my app will be broken until I move everything to saga.

For example, my app have a logic to send only after connect. (The code is simplified for readability, may not compile.)

function* connect() {
  const res = yield call(fetch, 'https://example.com/connect');

  if (res.ok) {
    const token = yield call(res.json);

    yield put({ type: 'CONNECTED', payload: { userID } });
  }
}

function* sendLoop() {
  for (;;) {
    const { payload: { userID } } = yield take('CONNECTED');

    const sendTask = yield fork(function* () {
      const sendAction = yield take('SEND');
      
      yield call(fetch, 'https://example.com/send/', { body: { userID } });
    });

    yield take('DISCONNECTED');
    yield cancel(sendTask);
  }
}

Then in my app side of the code, I have a middleware that do "send-on-connect":

({ dispatch }) => next => action => {
  if (action.type === 'CONNECTED') {
    dispatch('SEND');
  }

  return next(action);
}

The "send-on-connect" code will not work because sendLoop did not receive CONNECTED yet, so it lost the SEND action. To workaround, either:

  • I will need to have another action called PREPARE_CONNECTED, prime the sendLoop on this action
  • Move my "send-on-connect" code to saga
  • Buffer up actions before connect

I understand this could be by design, but I would like to understand the rationale behind.

Thanks.

@Andarist
Copy link
Member

The main rationale here is to ensure that select can get a state AFTER the action has a chance of changing the state in the reducers. This approach is used also by other saga-like libraries, i.e. redux-observable.

We had to decide on some logic there and this seems better for most cases, so unfortunately you'll have to work around it.

@feichao93
Copy link
Member

I think the main reason of disorder is the scheduler.

function sagaMiddleware({ getState, dispatch }) {
// ... other code
return next => action => {
  // ... other code...
  const result = next(action) // hit reducers
  channel.put(action) // LINE-A
  return result
}

Note that at LINE-A, the channel is an instance of stdChannel, and its put will be wrapped by asap, that means the actual action will not go through the underlying channel immediately when LINE-A executes. That's why the saga gets the action later than all other redux middlewares.

Sorry for using the implementation detail to explain this strange behavior, hope this could be understood by you guys.

@compulim
Copy link
Author

Thanks for the explanation. Good to know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants