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

Server Side Rendering (Universal) #13

Closed
timdorr opened this issue Dec 23, 2015 · 48 comments
Closed

Server Side Rendering (Universal) #13

timdorr opened this issue Dec 23, 2015 · 48 comments
Labels

Comments

@timdorr
Copy link

timdorr commented Dec 23, 2015

Do sagas handle server side rendering?

When on the server, there is a need to adapt async code into a synchronous flow, as we are handling a blocking server request that is waiting on some sort of data. If we're using React, we need to call ReactDOM.renderToString after we have our redux state populated. Often times that state is sourced from async sources, such as a REST API. So, we have to resolve those asynchronous processes back in the flow of the request.

I currently use Promises (and some custom data fetching dectorators) to handle this:

Promise.all(
  prefetchData(components, store)
).then(render);

That's a simplified form, but it's basically the same thing. My component decorator defines certain actions to send through Redux when calling prefetchData on it. It gets the promises from those actions (I use redux-promise currently) and bundles them up with Promise.all to let them be resolved before rendering.

Now, given a relatively complex saga, it looks like there isn't a sole promise to resolve. Is there, instead, some way we could watch for the completion of a set of sagas? That is, I could continue to dispatch my actions from my component decorators, followed by some call to the saga runner to wait for the generator functions we've invoked from those actions to finish. If so, I can use that as a means to get back to my synchronous flow and return a response to my blocking server request.

@yelouafi
Copy link
Member

Is there, instead, some way we could watch for the completion of a set of sagas?

If I understand, this is only related to 'startup sagas'. i.e. sagas that do some startup tasks (like initial data fetching) then termintates

Actually, the saga runner returns a promise, it's used internally by the middleware. But it's certainly possible to expose those promises to the outside.

A possible way is to return an array of promises attached as a prop to the middleware function

import { createStore, applyMiddleware } from 'redux'
import runSaga from 'redux-saga'

import sagas from '../sagas' // [startupSaga1, saga1, ...]

const sagaMiddleware = runSaga(...sagas)

const [startupPromise1, ...] = sagaMiddleware.promises

const createStoreWithSaga = applyMiddleware(
  sagaMiddleware
)(createStore)

@timdorr
Copy link
Author

timdorr commented Dec 23, 2015

That could work. What happens when one of those promises isn't invoked during startup?

@yelouafi
Copy link
Member

What do you mean by not invoked. All sagas are ran at startup. Do you mean sagas that run only on server ?

@timdorr
Copy link
Author

timdorr commented Dec 23, 2015

Sorry, I may be getting my terminology mixed up here.

So, say you have sagas for loading up users, products, and orders. If I visit the product page, then it will probably not fire actions that invoke the sagas for users or orders. So, if I want to Promise.all(sagaMiddleware.promises).then(render), I'm going to have promises in there that will never resolve.

That may be a contrived example. Does it get the point across well? If not, I can try to think up something more concrete.

@yelouafi
Copy link
Member

I understand (I think). You don't have to wait for all of them, but just pick the ones you want.

For example, say you have a root startup Saga that will fire all the bootstrap tasks

function *startupSaga() {

 // will terminate after all the calls resolve
  yield [
    call(fetchUsers),
    call(fetchOrders),
    ...
 ]
}

function* clientOnlySaga() { ... }

export default [startupSaga, clientOnlySaga, ...]

you can pick only the first one and wait for it

import sagas from '..'

const sagaMiddleware = runSaga(sagas) 
const [startupSaga, ...otherSagas] = sagaMiddleware.promises

startupSaga.then(render)

@dts
Copy link

dts commented Dec 30, 2015

Phrasing it another way: is there a way at any given time to see if there are any sagas outside of the "waiting to take" phase? This way you could, in the server-side renderer look and see if there are any outstanding processes, and wait for those to terminate (with a timeout race condition!) before releasing the HTML.

@slorber
Copy link
Contributor

slorber commented Dec 30, 2015

function *startupSaga() {

 // will terminate after all the calls resolve
  yield [
    call(fetchUsers),
    call(fetchOrders),
 ]
  yield put(applicationStarted())
}

@dts If you only render once the application started action gets fired, you know all the pending calls will have resolved. Not sure it's the best solution but is indeed a solution quite easy to setup.

@slorber
Copy link
Contributor

slorber commented Dec 30, 2015

@yelouafi I'm concerned by another problem we may have with sagas and server-side rendering.

When the backend renders we take a snapshot of the state and transmit it to the client.
But if a saga starts executing on the backend, then we can't transmit its current "state" easily to the frontend. I mean we loose at which point the saga was in the backend, and it will restart from the beginning once started on the frontend.

Let's consider a saga to handle the onboarding of a TodoMVC app.
Originally from reduxjs/redux#1171 (comment)

function* onboarding() {
  while ( true ) {
    take(ONBOARDING_STARTED)
    take(TODO_CREATED)
    put(SHOW_TODO_CREATION_CONGRATULATION)
    take(ONBOARDING_ENDED)
  }
}

If the ONBOARDING_STARTED gets fired on the backend then the saga on the backend will wait for a TODO_CREATED event.
Once the app starts on the frontend, the saga will start and now that frontend saga is still waiting for a ONBOARDING_STARTED event. You see what I mean?

@dts
Copy link

dts commented Dec 30, 2015

The downside of this is that you have to specify startup sagas as a specific category, which might be a different set for every different page. For me, sagas that get initiated during the routing/matching process that happens are "startup" sagas - a preferred syntax would be (modified form https://github.com/rackt/react-router/blob/master/docs/guides/advanced/ServerRendering.md)

import { renderToString } from 'react-dom/server'
import { match, RouterContext } from 'react-router'
import routes from './routes'
// this is made up, obviously:
import { activeSagas } from 'redux-saga'

serve((req, res) => {
  // Note that req.url here should be the full URL path from
  // the original request, including the query string.
  match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
    if (error) {
      res.status(500).send(error.message)
    } else if (redirectLocation) {
      res.redirect(302, redirectLocation.pathname + redirectLocation.search)
    } else if (renderProps) {
      waitToComplete(activeSagas).then(() => res.status(200).send(renderToString(<RouterContext {...renderProps} />)));
    } else {
      res.status(404).send('Not found')
    }
  })
})

What this entails is that some care needs to be taken with authentication and other long-lifed sagas such that they detect whether they are on the server side or client side (or be written and included separately), and do their business in slightly different ways:

Client auth:

while(true) {
if(!token) { yield take(SIGN_UP); yield put(authenticating) -> yield take(SIGN_OUT) }
else      { yield put(call(authorize,token))); yield take(SIGN_OUT) } 
}

Server auth:

if(token) { yield put(call(authorize,token); }

@dts
Copy link

dts commented Dec 30, 2015

@slorber - I don't think that half-executed sagas and server-side rendering are going to play nicely- we don't want to encode all aspects of the current state of the sagas (meta-state) in the state. I think the only reasonable solution is to have some sagas execute differently on the client and on the servers, which is quite straightforward - there is a list of sagas that is executed on the client, and a possibly intersecting but not identical set of sagas that run on the server. Resource fetching, for example, might have a much shorter timeout on the server than on the client.

There may be a need for some halfway piece, with some server-side sagas beginning and handing off to client side sagas, but I think the only clean way to mediate that is through officially ending the server-side saga, and picking up where it left off by leveraging the current state.

@yelouafi
Copy link
Member

@slorber

Once the app starts on the frontend, the saga will start and now that frontend saga is still waiting for a ONBOARDING_STARTED event. You see what I mean?

yes. And it seems inherent to the Saga approach.

@dts

I don't think that half-executed sagas and server-side rendering are going to play nicely

Unfortunately, that's true. While reducers can be started at any point in time (given a state and an action), Sagas need to start from the beginning.

Taking the onboarding example; care must be taken as @dts pointed to account on events that may have already occurred on server side

function* onboarding(getState) {
  while ( true ) {
    if( !getState().isOnBoarding )
      take(ONBOARDING_STARTED)
    take(TODO_CREATED)
    put(SHOW_TODO_CREATION_CONGRATULATION)
    take(ONBOARDING_ENDED)
  }
}

But I'm pretty sure you see the issues with this approach. We can't start the onboarding saga from some arbitrary point (for example from the take(TODO_CREATED) point). We'd have to specify from what point we would take the lead.

another possible solution is somewhat related to #22. If we record the event log on the server. We can replay the Saga on the client: replay means we will advance the saga with the past event log and the recorded responses from the saga on the server

@slorber
Copy link
Contributor

slorber commented Dec 30, 2015

yes I was thinking of something similar, but while you replay you'll probably want to stub the side-effects. It becomes quite complicated... The same problem may appear if you try to hot reload the saga.

I don't think the problem is inherent to sagas but rather sagas implemented with generators.

@yelouafi
Copy link
Member

yes I was thinking of something similar, but while you replay you'll probably want to stub the side-effects. It becomes quite complicated

Maybe not. on this post @youknowriad mentioned the term pure generators to denote the fact that sagas dont execute the effects themeselves. This gave me some ideas

  • a pure function is a function that - given the same inputs - will always produce the same outputs

  • a pure iterator is an iterator that - given the same sequence of inputs - will always produce the same sequence of outputs.

    I m aware the 2nd definition isn't quite exact, because reexecuting effects yielded by the iterator can lead to different results. But we can assign an ID to each newly started saga - as @gaearon mentioned in DevTools for Sagas #5 (comment) - the ID will be an ordinal - i.e. 5 means the 5th started saga.

Same thing for yielded effects, assign an ordinal num. to each yielded effect.

If we know that sagas/effects are always triggered in the same order

  • on the run phase, we can record all called/forked sagas and yielded effects as well as the results of running each effect.
  • on the replay phase, an iterator player will iterate on each iterator getting yielded effects/sagas. for each triggered effect it will locate the previously recorded response by the pair sagaNum/effectNum and resume the iterator with it

I know this sounds a bit theoritical right now. But if the 'effects are always triggered in the same order' assumption can hold for most cases, I think the above solution is doable

@gaearon
Copy link
Contributor

gaearon commented Dec 31, 2015

This sounds quite complicated and error prone. I'd vote for running different sagas on the server and the client. Is there any problem with this approach?

@yelouafi
Copy link
Member

Is there any problem with this approach?

Not AFAIK, but the only cases I saw with universal apps involved doing an initial render on the server given some route and sending the initial state + UI to the client.

But this also means it'd be hard or complicated to do hot relaoding/time travel on Sagas

@slorber I m bit curious on how an event like ONBOARDING_STARTED would fire on the server.

@slorber
Copy link
Contributor

slorber commented Dec 31, 2015

Actually I've not come up with any better usecase so maybe it's just a non issue. The users of redux saga should just be aware of the drawbacks and that the saga's progress will be lost when transferring from server to client.

We don't do server side rendering yet so it's hard for me to find a usecase in my app right now.

Imho a saga running on the server could probably always/easily be entirely replaced by simple action dispatching. As the server must respond to the client quickly generally only "boostrap events" are fired at this stage and you can easily deal with these with a short-lived/server-only boostrap saga, or simply not use a saga at all and dispatch the required actions by yourself.

@dts
Copy link

dts commented Jan 2, 2016

The saga methodology is IMHO perfect for a lot of different client/server asymmetries. On the server side, an externally-similar saga does resource fetching compared with the client. The server-side saga would have different timeouts and error management, but the trigger and effect would be the same or similar. When some component wants a resource, it triggers an action that on the server triggers one saga, and on the client triggers a different one. I love the simplicity of this approach, and I wouldn't call it a downside, I'd call it, "let's keep shit simple, people".

@yelouafi
Copy link
Member

yelouafi commented Jan 2, 2016

After all the comments above. I think the preferred way is to provide an external method runSaga. So we don't have to provide 2 different store configs for the client and the server

runSaga(saga, store).then(...)

the store argument will allow the saga to still dispatch actions to the store on the server (and possibly take actions from it)

Do you think this method is better than returning the end promise directly from the middleware function #13 (comment)

@dts
Copy link

dts commented Jan 3, 2016

So the whole API would be to mount saga middleware, just without the list of sagas, then call this when you want to fire up individual sagas? This makes sense to me. This way, on the server-side you say runSaga(fetchResource,store).then(renderToString)
Where fetchResource forks off into as many as needed (each fork racing against a timeout), and the global waiting for a DONE_ROUTING signal, at which point it cuts off waiting for more fetches.

@yelouafi
Copy link
Member

yelouafi commented Jan 4, 2016

Added runSaga support to the master branch. With a slight modification; the function returns a task instead of the promise (so we could cancel the task when cancellation support will be enabled)

const task = runSaga( someSaga(store.getState), store )

task.done.then( renderAppToString )

The same method can also be used on the client (to handle use cases issued in #23) Here is an example of running the saga from outside in the async example (It feels the 2 bootstrap methods -runSaga and middleware- are somewhat redundant)

This is not released yet on npm. I'm waiting for any feedback before making a release

@ashaffer
Copy link

ashaffer commented Jan 4, 2016

IMO trying to serialize the state of a saga/effect is the wrong approach. It's complicated, error-prone, and unlikely to capture the exact semantics that you want all the time.

A better approach, I think, is to create some function of state that decides when to complete the rendering process on the server. E.g. isReady(state) is true when no relevant sagas are in progress. This is the approach i'm trying to take with vdux-server.

If you take this approach, then it is simply the saga's responsibility to let things know when its done, if this particular saga is one that you want to be completed server-side first. If it's a saga you don't care about, then it doesn't need to contribute to this loading state.

This also neatly addresses problems like certain sagas not necessarily beginning immediately and therefore possibly being skipped if you tried to collect all promises, for instance.

EDIT: In thinking about it a bit more, it does seem like there is possibly an exceptional case for sagas that are non-transient. Like if there is some kind of saga that you want to begin server-side, and persists indefinitely on the client, but you wish to restore it's state. This seems like a weird case though. Are there any actual examples of something like this being desirable?

@dts
Copy link

dts commented Jan 4, 2016

@yelouafi - will task.done.then() only return once all forks are finished, or only when the main one in question is completed?

@yelouafi
Copy link
Member

yelouafi commented Jan 4, 2016

@dts it will only resolve with the main one. If you want to wait on some or all forked sagas, you have to use join to wait their termination

@yelouafi
Copy link
Member

yelouafi commented Jan 4, 2016

@ashaffer

A better approach, I think, is to create some function of state that decides when to complete the rendering process on the server. E.g. isReady(state) is true when no relevant sagas are in progress.

This is what @slorber suggested (#13 (comment)).

But with runSaga I think @gaearon solution (#13 (comment)) will work for most cases without having to maintain 2 saga middleware configs (client and server). I don't really like the monkey patching solution used to make this work. But as mentioned in #23 runSaga maybe necessary for apps using code splitting (i.e. we don't know all the sagas to run at the start time).

If somoene has a better idea I'll take it, but for now runSaga seems to solve both issues.

@tappleby
Copy link

tappleby commented Jan 5, 2016

I don't really like the monkey patching solution used to make this work.

@yelouafi Since you need a store instance to run a saga this could be a good use case for a store enhancer instead of middleware:

export function reduxSagaStoreEnhancer(...startupSagas) {
  return next => (...args) => {
    const store = next(...args)
    const sagaEmitter = emitter()

    function dispatch(...dispatchArgs) {
      const res = store.dispatch(...dispatchArgs)
      sagaEmitter.emit(action)
      return res
    }

    function runSaga(iterator) {
      check(iterator, is.iterator, NOT_ITERATOR_ERROR)

      return proc(
        iterator,
        sagaEmitter.subscribe,
        store.dispatch,
        action => asap(() => dispatch(action))
      )
    }    

    const sagaStartupTasks = startupSagas.map(runSaga)

    return {
      ...store,
      dispatch,
      runSaga,
      sagaStartupTasks
    }
  }
}

Usage:

const finalCreateStore = compose(
  applyMiddleware(...middleware),
  reduxSagaStoreEnhancer(...startupSagas);
);

const store = finalCreateStore(...);

store.runSaga(iterator);

An added bonus of using a store enhancer is you can expose the promises for startup sagas:

Promise.all(store.sagaStartupTasks).then(renderToString)

@yelouafi
Copy link
Member

yelouafi commented Jan 6, 2016

@tappleby your solution makes more sens. It has also the benefit of patching the store only once (i.e. with multiple calls to runSaga). My only concern is with using store enhancers themselves. If someone would use a store config like this

const finalCreateStore = compose(
  applyMiddleware(...middleware),
  reduxSagaStoreEnhancer(...startupSagas),
  reduxRouter(),
  devtools.instrument()
);

that's perhaps too much enhancers. I dont remember exactly where, but I learned that using multiple store enhancers may present some issues with action dispatching, because each store enhancer has its own dispatch method, but I m not sure if this applies to the present use case.

@tappleby
Copy link

tappleby commented Jan 6, 2016

Yeah ordering can be an issue, since they are composed from right to left any enhancer that comes after in the chain that uses dispatch wouldnt trigger sagaEmitter.emit(action). I recently had to document this in redux-batched-subscribe... This is where I wish redux almost had more hook points for extending it eg. beforeDispatch, afterDispatch etc.

@pavelkornev
Copy link
Contributor

In my opinion it would be perfect if components don't even know about sagas existence. Components should operate only with actions. But i can't figure out an appropriate way how can we distinguish on the server the moment when all sagas are done. I mean, we can register all sagas on the server like we do on the client, then emit actions to run them, but when to render? I will appreciate if someone can come up with ideas or with ready to use solution :-)

@yelouafi
Copy link
Member

@pavelkornev

I mean, we can register all sagas on the server like we do on the client, then emit actions to run them, but when to render?

you can run your server sagas from outside using middleware.run

middleware.run(saga) will return a Task object. You can use task.done which is a Promise that resolves when the Saga ends, or rejects when the Saga throws an error

import serverSagas from './serverSagas'
import sagaMiddleware from './configureStore'

const tasks = serverSagas.map(saga => sagaMiddleware.run(saga))
tasksEndPromises = tasks.map(t => t.done)

Promise.all(tasksEndPromises).then(render)

@pavelkornev
Copy link
Contributor

@yelouafi i do exactly what you have just described. But it does not solve the problem i've mentioned above.

How can we determine which tasks (serverSagas) we should run for getting data to render particular page? The only way i see is asking component's what they need by shaking tree of components which we have in call callback of match function from react-router. But question is — what should return components — task or action? In my point of view, the power of redux-saga is that it can make components more abstractive from data loading and all async stuff which means components should not even know about sagas existence; components should operate only with actions. In other words it should return an action. I hope it sounds logically. Now let's see how we can implement it step by step:

  1. First step is easiest one. As many solutions for server-side data fetching suggest, we can write static method. This method will simply dispatch actions or set of actions.
  2. We need somehow be aware which sagas to run. We can't figure this out by those actions which our components have just emitted in Step 1 since actions don't depend on sagas at all. Easiest way is to register all serverSagas at once, but we need to filter those of them we don't need. In other words, we need to exclude those tasks which was not triggered by emitted actions because our Promise.all will never be resolved with them. And that is a problem.

I hope it's clear now.

@ganarajpr
Copy link

Is there an example somewhere that shows how we could use saga with a universal app ?

I am currently working on a universal app and I wanted to integrate saga with it. Any examples or pointers would be highly appreciated.

@pavelkornev
Copy link
Contributor

@yelouafi what do you think if i make an example of our approach of universal saga usage and PR?

@yelouafi
Copy link
Member

@pavelkornev That would be great! thanks

@Dattaya
Copy link

Dattaya commented Feb 18, 2016

@pavelkornev thank you so much for your response and example. I've been busy with other stuff, so haven't had a change to try it, but I'm definitely going to.

@prashaantt
Copy link
Contributor

@pavelkornev Please do, that would be amazing! I was just going to ask you for the same on this thread.

pavelkornev added a commit to pavelkornev/redux-saga that referenced this issue Feb 23, 2016
@pavelkornev
Copy link
Contributor

I've just committed the approximate solution. If you disable JavaScript in the browser you will clearly see that server return page with necessary data:

Screenshot

DevTools has been disabled (see file ./containers/Root.dev.js) since there is an error around it:

Warning: React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server:
 (client) 1.1.$/=10.1.$1.0.0">LOAD_USER_PAGE</div>
 (server) 1.1.$/=10.1.$1.0.0">USER_REQUEST</div><d

It's not that obvious how to fix it. I would ask @gaearon as an author of these dev tools for an advise.

@yelouafi
Copy link
Member

see #255.

@yelouafi
Copy link
Member

@pavelkornev

It's not that obvious how to fix it. I would ask @gaearon as an author of these dev tools for an advise.

FYI, this solved it for me. The solution is to wrap the server-rendered react markup in an additional div

http://stackoverflow.com/questions/33521047/warning-react-attempted-to-reuse-markup-in-a-container-but-the-checksum-was-inv?answertab=active#tab-top

@pavelkornev
Copy link
Contributor

pavelkornev commented Jul 4, 2016

@casertap my solution is obsolete. Please use END effect. More details in #255.

@casertap
Copy link

casertap commented Jul 4, 2016

@pavelkornev ok thanks for the link

@GaddMaster
Copy link

That could work. What happens when one of those promises isn't invoked during startup?

Wrap them in a second promise and resolve if false or true, so ether way there resolved

SantoJambit pushed a commit to SantoJambit/redux-saga that referenced this issue May 14, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests