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 strategy #70

Closed
lukewestby opened this issue Jun 23, 2016 · 9 comments
Closed

server-side rendering strategy #70

lukewestby opened this issue Jun 23, 2016 · 9 comments

Comments

@lukewestby
Copy link
Contributor

As a part of redux-loop 3.0 I want to have a solid strategy that can be communicated about how to use reducers that return effects as a means of implementing the initial data fetch for server-rendered apps.

The prior discussions, proposed code changes, and outstanding issues around this to date are:

There are two techniques I want to propose to start, my favorite as of now is the first:

  1. Lean on existing functionality and write documentation on how to use it for server rendering
  • No additional methods are added to the store
  • Passing a loop to the initialState argument of createStore is explicitly disallowed by throwing an error if it is detected
  • Recommend using an INITIALIZE_APP action that returns the appropriate effects for getting the store where it needs to be for rendering to be considered complete
  • Rely on the fact that store.dispatch() returns a Promise for when the effect tree of the dispatched action has completed
  1. Allow subscriptions to effect completion

Thoughts?
@bardt @mocheng @bdwain

@bdwain
Copy link
Member

bdwain commented Jun 23, 2016

I'll check this out in detail more later and give my thoughts. Though i don't have a lot of experience with server-side rendering

@bardt
Copy link

bardt commented Jun 24, 2016

I experimented with server rendering a lot recently. I found redux-loop difficult to understand for my team on scenarios with many async steps, so switched from it right now. But during my experiments I came to alternative approach:

  1. Add a reducer to manage effects count in state
  2. Dispatch reserved action EFFECT_STARTED right before the effect is started
  3. Dispatch reserved action EFFECT_FINISHED in .then step after effect is finished
  4. Look into state.effects.count inside the store listener and make decisions differently on server and in browser.

This way we reuse all the redux ecosystem, including DevTools, "as is". We can see when effects start and finish by looking at store on each change.

Offtopic:

Another thing I found useful, no matter on server or in client, is to be able to look at the whole store object inside the effects. Given you have other enhancers, which not just modify, but add new functions to store (like dependency injection), this will make effects creators more flexibility. In my case, I had to give the store different implementation of API layer on server and on client, so this was critical.

Probably we could think about it and include this ability into 3.0. For this purpose I implemented another effect type in my fork: https://github.com/bardt/redux-loop/blob/build-effect/modules/effects.js#L31. This is a naive approach, so we could make it better together.

@bdwain
Copy link
Member

bdwain commented Jun 24, 2016

@bardt i also found that i sometimes needed pieces of the state tree besides the calling reducer in an effect. i made middleware, https://github.com/bdwain/redux-action-enhancer, to handle that. Basically it lets you enhance actions with values from the state with a mapState function, similar to mapStateToProps. so the action will have everything from the state the effect needs.

@bdwain
Copy link
Member

bdwain commented Jun 25, 2016

@lukewestby i like the idea of option 1 except the recomendation of an INITIALIZE_APP action, it's kind of a special case action that you need to add support for when you could otherwise dispatch normal actions.

What about recommending to dispatch as many requests as you want, and then just using Promise.all on all of the returned promises.

let promises = [];
promises.push(store.dispatch(getAction1()));
promises.push(store.dispatch(getAction2()));

Promise.all(promises).then(() => { sendResponse(html, store.getState()) };

@scottnonnenberg
Copy link

scottnonnenberg commented Jul 10, 2016

Hey everyone. I've been thinking a whole lot about the combination of error handling and server side rendering with redux-loop. My primary concern is that properly-written async action creators, with both a .then() and a .catch() will never result in a rejected promise. In fact, install() really doesn't like it when Effect promises are rejected: https://github.com/raisemarketplace/redux-loop/blob/26c8e79db2813f11514bdba56e696e5769ea59af/modules/install.js#L43

This works fine in the browser. You make a request to an API, and your spinner will be left spinning until you either return a result or show that an error happened. Makes sense.

However, on the server things are a little different. A request comes in at a certain URL, and you determine the components to be rendered and the async action creators to be called. You dispatch() those, waiting for their promises to be resolved. Then you render your components with a store in good shape.

But what if one of those async server-side data requests fails? The promise had a .catch() and your top-level promises will never be rejected. You render a page filled with errors back to the user.

We could rewrite our action creators to reject on the server and .catch() on the client. But that's a lot of logic to inject into a lot of action creators. It might be better to structure the returned failure actions to have some sort of error key, checked for here: https://github.com/raisemarketplace/redux-loop/blob/26c8e79db2813f11514bdba56e696e5769ea59af/modules/install.js#L40

What do you guys think? Am I missing something?

@bdwain
Copy link
Member

bdwain commented Jul 12, 2016

@scottnonnenberg why does the top level promise need to reject in that scenario. If an async action fails, it should dispatch an action setting some error state, But the whole application isn't really in an error state. Just that part of the state tree. That error can just be handled as normal with redux and react (or whatever you use) and be displayed to the user can't it?

@scottnonnenberg
Copy link

While I can see some situations where I'd be alright returning a 200 success with a little error embedded inside the page, that wouldn't be appropriate all the time. I expect some sort of release valve so I can return a non-200 error code and a full-page 'site is not useful right now' error.

Perhaps an option passed to redux-loop on install(), combined with a certain shape of error actions?

@lukewestby
Copy link
Contributor Author

@scottnonnenberg can you just inspect some part of the state after finishing your setup effects but prior to responding to the request and use that to set the status code? In my experience with server-side rendering the store will be available and therefor getState() at the time you need to call res.send(), correct?

@scottnonnenberg
Copy link

@lukewestby Yes, as long as every reducer saves the error on the state. Totally doable.

But I'm interested in creating solutions which don't require others to know anything as they implement their work in a different part of the app. Action creators returning an error key conforms to the standard flux action spec, easy, no-brainer. The alternative would be to standardize on some shape for all reducer-produced state. I don't like the education I'd need to do with all people on the project writing reducers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants