redux-saga: Idea: Sagas 'returning' promises from middleware (to support Server Side Rendering)

Last night I was working with sagas, doing standard-fare authentication flow. However, I use server rendering, and sagas seem to be lacking for this.

In my existing apps, I do something like dispatch(login(email, password)).then(/*...*/). I then proceed to render the route and send a response. Of course, with sagas it’s not as simple.

I have read #13 and runSagas is a good option to have. However, what if we consider an effect that alters the response of dispatch on certain actions?

Let’s call it takeDefer for now. A better name would be nice if this idea is viable

function doLogin(email, password) {
  const promise = dispatch({ type: 'LOGIN',  payload: { email, password } });
  promise.then(/*...*/).catch(/*...*/);
}

function * login() {
  while (true) {
    // Right now, dispatch(LOGIN) would return a POJO
    // Once the generator yields `takeDefer` to the middleware, it latches
    // on and alters the return value of `dispatch(LOGIN)`

    const task = yield takeDefer('LOGIN'); 

    // At this point, subsequent dispatch(LOGIN) would not return a promise

    const { username, password } = task.action.payload;
    const authTask = yield fork(authorize, username, password);

    const { fail } = yield race({
      success: take('LOGIN_SUCCESS'),
      fail: take('LOGIN_FAIL'),
    })

    if (fail) {
      task.reject('Login has failed')
    } else {
      task.resolve('Login complete!')
    }

    yield take('LOGOUT');
  }
}

If there were multiple takeDefer('LOGIN') effects concurrently waiting, the promise returned from the middleware would be Promise.all(...). The array of promises could be made available as well.

I believe this is possible, and would love to give a whack at a PR. However, does this sound sane? Does this kind of usage of sagas break their intent?

From a practical standpoint, being able to leverage promises like this could be very useful for server rendering–I could reuse the same client-saga on the server, and jump out of it once I have what is needed. Also, it would help with CPS methods such as onEnter in React Router.

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Comments: 18 (17 by maintainers)

Most upvoted comments

@yelouafi at first blush, I like this approach. I’ll try and provide some more thoughtful feedback soon.

Thanks for putting so much effort into this! If you happen to be going to React Europe, I’ll buy you a beer!

@quicksnap @pavelkornev I’ve a proposed solution here. In https://github.com/yelouafi/redux-saga/issues/78#issuecomment-203886961 I propose that a parent is terminated only when all attached forked tasks terminate. If we combine this with a special END action that I intend to introduce. We can have soemthing like this

function* watcher() {
  let action
  while((action = take('ACTION') !== END) {
    yield fork(worker, action.payload)
  }
}

function* rootSaga() {
  yield [
    fork(watcher),
    fork(anotherWatcher)
  ]
}


// on the server rendering code, inside match

const sagaRunner = createSagaMiddleware() // no saga is passed here
const store = createStore(reducer, applyMiddleware(sagaRunner))
const rootTask = sagaRunner.run(rootSaga)

renderToString(RootComponent)
// will break the while loop on watchers
store.dispatch(END)

// rootTask will resolve only when all tasks in the program are terminated
rootTask.done.then(/* render and send state + html to the client */)

END is a special action, it notifies all takers that no more actions will be dispatched. And since a saga won’t terminate until all forked tasks terminate, we can use the done promise of the root saga to determine when all forked tasks are completed. Similarly the root saga will abort on the first error encountered by a forked task so we can use this to send an error to the client.

W’ll be rendering twice of course on the server, but The main advantage here is that the same saga code would run on the server and client