Giter Club home page Giter Club logo

Comments (48)

pavelkornev avatar pavelkornev commented on May 28, 2024 4

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.

pavelkornev avatar pavelkornev commented on May 28, 2024 3

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.

from redux-saga.

pavelkornev avatar pavelkornev commented on May 28, 2024 2

@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.

from redux-saga.

pavelkornev avatar pavelkornev commented on May 28, 2024 2

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

from redux-saga.

dts avatar dts commented on May 28, 2024 1

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.

yelouafi avatar yelouafi commented on May 28, 2024 1

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.

yelouafi avatar yelouafi commented on May 28, 2024 1

@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

from redux-saga.

pavelkornev avatar pavelkornev commented on May 28, 2024 1

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

from redux-saga.

yelouafi avatar yelouafi commented on May 28, 2024

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.

timdorr avatar timdorr commented on May 28, 2024

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

from redux-saga.

yelouafi avatar yelouafi commented on May 28, 2024

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.

timdorr avatar timdorr commented on May 28, 2024

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.

yelouafi avatar yelouafi commented on May 28, 2024

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.

dts avatar dts commented on May 28, 2024

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.

slorber avatar slorber commented on May 28, 2024
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.

slorber avatar slorber commented on May 28, 2024

@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.

dts avatar dts commented on May 28, 2024

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.

dts avatar dts commented on May 28, 2024

@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.

yelouafi avatar yelouafi commented on May 28, 2024

@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

from redux-saga.

slorber avatar slorber commented on May 28, 2024

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.

yelouafi avatar yelouafi commented on May 28, 2024

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 #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.

gaearon avatar gaearon commented on May 28, 2024

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.

yelouafi avatar yelouafi commented on May 28, 2024

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.

slorber avatar slorber commented on May 28, 2024

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.

yelouafi avatar yelouafi commented on May 28, 2024

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.

dts avatar dts commented on May 28, 2024

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.

yelouafi avatar yelouafi commented on May 28, 2024

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.

ashaffer avatar ashaffer commented on May 28, 2024

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.

dts avatar dts commented on May 28, 2024

@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.

yelouafi avatar yelouafi commented on May 28, 2024

@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.

yelouafi avatar yelouafi commented on May 28, 2024

@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.

from redux-saga.

tappleby avatar tappleby commented on May 28, 2024

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.

yelouafi avatar yelouafi commented on May 28, 2024

@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.

tappleby avatar tappleby commented on May 28, 2024

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.

yelouafi avatar yelouafi commented on May 28, 2024

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.

pavelkornev avatar pavelkornev commented on May 28, 2024

@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.

pavelkornev avatar pavelkornev commented on May 28, 2024

@yelouafi thanks 👍

from redux-saga.

Dattaya avatar Dattaya commented on May 28, 2024

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.

pavelkornev avatar pavelkornev commented on May 28, 2024

@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.

ntkoso avatar ntkoso commented on May 28, 2024

I'm using almost the same solution, but instead generators / sagas fecher functions return fork effects.
gist

from redux-saga.

yelouafi avatar yelouafi commented on May 28, 2024

@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)

from redux-saga.

ganarajpr avatar ganarajpr commented on May 28, 2024

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.

yelouafi avatar yelouafi commented on May 28, 2024

@pavelkornev That would be great! thanks

from redux-saga.

Dattaya avatar Dattaya commented on May 28, 2024

@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.

prashaantt avatar prashaantt commented on May 28, 2024

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

from redux-saga.

yelouafi avatar yelouafi commented on May 28, 2024

see #255.

from redux-saga.

casertap avatar casertap commented on May 28, 2024

@pavelkornev ok thanks for the link

from redux-saga.

GaddMaster avatar GaddMaster commented on May 28, 2024

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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.