Comments (48)
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 :-)
from redux-saga.
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:
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.
from redux-saga.
@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:
- 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. - 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.
from redux-saga.
@yelouafi what do you think if i make an example of our approach of universal saga usage and PR?
from redux-saga.
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".
from redux-saga.
join
can be called only with one task. In your code yield [...]
returns an array of tasks so you've to write something like
function* rootSaga() {
// yield array of forks -> array of tasks
const taskList = yield [
fork(subtask1, ...args),
fork(subtask2, ...args),
...
]
// yield array of joins -> will return after all tasks terminate
yield taskList.map(join)
}
from redux-saga.
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
from redux-saga.
@casertap my solution is obsolete. Please use END
effect. More details in #255.
from redux-saga.
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)
from redux-saga.
That could work. What happens when one of those promises isn't invoked during startup?
from redux-saga.
What do you mean by not invoked. All sagas are ran at startup. Do you mean sagas that run only on server ?
from redux-saga.
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.
from redux-saga.
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)
from redux-saga.
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.
from redux-saga.
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.
from redux-saga.
@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?
from redux-saga.
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); }
from redux-saga.
@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.
from redux-saga.
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.
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
from redux-saga.
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.
from redux-saga.
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. Butwe can assign an ID to each newly started saga - as @gaearon mentioned in #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
from redux-saga.
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?
from redux-saga.
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.
from redux-saga.
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.
from redux-saga.
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)
from redux-saga.
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.
from redux-saga.
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
from redux-saga.
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?
from redux-saga.
@yelouafi - will task.done.then() only return once all forks are finished, or only when the main one in question is completed?
from redux-saga.
@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
from redux-saga.
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.
from redux-saga.
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)
from redux-saga.
@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.
from redux-saga.
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.
from redux-saga.
I'll close this as the current version added the standalone runSaga function. Feel free to comment. I can reopen the issue if needed
from redux-saga.
@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 could you give an example with fork
/join
? Is it something like the that?
function* rootSaga() {
const task = yield [
fork(subtask1, ...args),
fork(subtask2, ...args),
...
]
yield join(task)
}
const task = runSaga( rootSaga(store.getState), store )
task.done.then( renderAppToString )
from redux-saga.
@yelouafi thanks 👍
from redux-saga.
Does anyone have an example of a universal app with redux-saga
? Or maybe I don't need it on the server since there is no user interaction there and I can process my simple LOAD
actions as it was before with a special redux middleware that returns a promise?
from redux-saga.
@Dattaya i can describe what we've done in our app. We've created simple sagas which don't listen for a specific action to happen. When request happens, components return lists of sagas they need to perform for prefetching data. Then we compose this list of sagas in one root saga and run it. Afterwards, when all sagas are completed, we render the page. On the other side, on client side, we wrap all these simple sagas in wrappers which are waiting for a specific action to happen, we call them 'watcher'. We took some ideas from this example - https://github.com/yelouafi/redux-saga/blob/master/examples/real-world/sagas/index.js
from redux-saga.
I'm using almost the same solution, but instead generators / sagas fecher functions return fork effects.
gist
from redux-saga.
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)
from redux-saga.
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.
from redux-saga.
@pavelkornev That would be great! thanks
from redux-saga.
@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.
from redux-saga.
@pavelkornev Please do, that would be amazing! I was just going to ask you for the same on this thread.
from redux-saga.
see #255.
from redux-saga.
@pavelkornev ok thanks for the link
from redux-saga.
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
from redux-saga.
Related Issues (20)
- Waiting for an action with takeMaybe / take after END is dispatched for SSR HOT 7
- Is it possible to selectively cancel tasks in an actionChannel? Ie cancel the 3rd task out of 5 running ones. HOT 5
- Is it possible for a saga to "trace" the effect "chain"? HOT 4
- Delay inside of while loop may never fire with React Native 0.71.6 HOT 2
- UI freezes when chrome devtools is open HOT 4
- Redux 4.0 - Unable to access updated data using useSelector HOT 2
- could we add leading/trailing edge options for debounce? HOT 3
- Workflow has flaw
- Why not use the await and async instead of the generator and yield? HOT 1
- TS2345 error while putting thunk actions
- React native Redux Saga with Redux Tollkit
- Module '"redux-saga/effects"' has no exported member 'call'. HOT 4
- Is there a standard way to break while true loops with call effect when END is dispatched? HOT 1
- Can put type improvements be released downstream? HOT 2
- Sending very large files, tasks in parallel are using a lot of memory
- How to use package that use redux-saga as dependency when its in webpack externals? HOT 7
- Help me connect redux-saga with Nextjs 13.5 using app router HOT 2
- Update peer dependencies to include `redux@5` (currently beta) HOT 14
- feature request: interface for integration with other frameworks (like Vue) HOT 2
- Redux saga is not working in apps script react js project HOT 2
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from redux-saga.