-
-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Comments
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) |
That could work. What happens when one of those promises isn't invoked during startup? |
What do you mean by not invoked. All sagas are ran at startup. Do you mean sagas that run only on server ? |
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 That may be a contrived example. Does it get the point across well? If not, I can try to think up something more concrete. |
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) |
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. |
@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. |
@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. Let's consider a saga to handle the onboarding of a TodoMVC app. function* onboarding() {
while ( true ) {
take(ONBOARDING_STARTED)
take(TODO_CREATED)
put(SHOW_TODO_CREATION_CONGRATULATION)
take(ONBOARDING_ENDED)
}
} If the |
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)
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:
Server auth:
|
@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. |
yes. And it seems inherent to the Saga approach.
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 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 |
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. |
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
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
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 |
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? |
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 |
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. |
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". |
After all the comments above. I think the preferred way is to provide an external method runSaga(saga, store).then(...) the Do you think this method is better than returning the end promise directly from the middleware function #13 (comment) |
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 |
Added 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 |
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. 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? |
@yelouafi - will task.done.then() only return once all forks are finished, or only when the main one in question is completed? |
@dts it will only resolve with the main one. If you want to wait on some or all forked sagas, you have to use |
This is what @slorber suggested (#13 (comment)). But with If somoene has a better idea I'll take it, but for now |
@yelouafi Since you need a 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) |
@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. |
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 |
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 :-) |
you can run your server sagas from outside using middleware.run
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) |
@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
I hope it's clear now. |
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. |
@yelouafi what do you think if i make an example of our approach of universal saga usage and PR? |
@pavelkornev That would be great! thanks |
@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. |
@pavelkornev Please do, that would be amazing! I was just going to ask you for the same on this thread. |
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: DevTools has been disabled (see file ./containers/Root.dev.js) since there is an error around it:
It's not that obvious how to fix it. I would ask @gaearon as an author of these dev tools for an advise. |
see #255. |
FYI, this solved it for me. The solution is to wrap the server-rendered react markup in an additional div |
@pavelkornev ok thanks for the link |
Wrap them in a second promise and resolve if false or true, so ether way there resolved |
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:
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 useredux-promise
currently) and bundles them up withPromise.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.
The text was updated successfully, but these errors were encountered: