ioof-holdings / redux-dynostore

These libraries provide tools for building dynamic Redux stores.
BSD 3-Clause "New" or "Revised" License
122 stars 15 forks source link

SSR compatibility #21

Closed klis87 closed 3 years ago

klis87 commented 5 years ago

Is it a bug, feature request or question?

Question/Feature request

Which package(s) does this involve?

Probably every package, but mostly react-redux binding

Expected Behavior

Does this library support server side rendering? I know SSR + code splitting is quite a tricky combination, but thx for libraries like react-loadable or loadable-components it is possible now for React. I am wondering whether this is possible together with dynostore and if not, whether it would be difficult to achieve.

mpeyper commented 5 years ago

To be honest, I've never done SSR myself and don't know what is involved, what the pitfalls are and what might make dynostore incompatible.

Essentially, all dynostore does is add a redux enhancer and accesses the store in React context to call some functions when the component did mount, so I think if all those pieces are SSR compatible, it should just work.

If you want to set up an example (even submit it as a PR?) and it doesn't work, I'll gladly take a look.

jpeyper commented 5 years ago

(i know Michael already answered, but I write this out on my phone in bed so I didn't want to waste it).

SSR is not something we use (yet) so it has not been tested or a focus of this library.

I can't see why the core packages wouldn't work (in theory) although I do know there are tricky bits about SSR that could trip us up. The more specific package for sagas (or other compatibility enhancers/plugins that's others may have written) would be dependent on the other library supporting it.

I won't get time today, but I'll see if I can get an example working soon (or you can submit one via a PR and we can take a look)

mpeyper commented 5 years ago

Also, just to clarify, dynostore does not do any code splitting itself. Usually you would wrap the top level component inside the split chunk in dynamic so that it wires itself into the store after the chunk has loaded and the component is mounted (using something like react-loadable and webpack's dynamic import).

jpeyper commented 5 years ago

We have an example of using react-loadable in our micro-frontends example.

I don't really want to dilute this question with that issue, so if you have questions about code splitting and dynostore, let's do it in another issue.

mpeyper commented 5 years ago

And the real world example.

klis87 commented 5 years ago

@jpeyper @mpeyper thx for your fast answers, now I can see that code-splitting won't cause any problems for SSR. There are different problems though. I checked your documentation and I suspect that SSR won't be possible for now. To be able to do SSR, we need 2 things:

1) ability to serialize state on server side, inject it into index.html, and then, on client side, deserializing it and injecting in global reducer as initial state

I am not sure about this one, in pure redux you can pass it like createStore(rootReducer, initialState), but I am not sure how it would work for dynamic reducers. For instance, lets say that you state has the following form:

{
  static: {} // created with a static reducer
  dynamic: {} // created with a dynamic reducer, during component mount
}

Assuming that dynamic reducer was generated on server side, would dynamic reducer on client side pick dynamic state passed in createStore?

2) ability to populate the store before React rendering on the server side, mostly with responses from AJAX requests.

Usually this is achieved by adding static methods to top level components, which for instance dispatch redux thunks which return promise, which u wrap in Promise.all after which your store will be populated.

For Redux-Saga, there are different possibilities, for instance https://github.com/redux-saga/redux-saga/blob/master/examples/real-world/store/configureStore.dev.js#L32 and https://github.com/redux-saga/redux-saga/blob/master/examples/real-world/server.js#L59, so your sagas can do required job before u start rendering, again to have the store populared.

I guess this is not possible to do, right? We would need a mechanism to await something before rendering component passed to dynamic HOC, like promise resolved or an action dispatch.

mpeyper commented 5 years ago

This is where my knowledge of SSR is falling short as I don't think I know enough to actually help here...

I am not sure about this one, in pure redux you can pass it like createStore(rootReducer, initialState), but I am not sure how it would work for dynamic reducers.

In theory, this should work. ~In practice, there is bug in redux-subspace that would probably prevent this from working as expected. It has been fixed in a PR but not released yet~ Edit: this has been released now.

The dynamic section of the state should get ignored by the static reducer and passed to the dynamic reducer when replaceReducer is eventually called.

The bit I'm unsure of is if the dynamic component will get a chance to attach again once it hits the client. I'm assuming the idea with Redux in SSR is that you just reconstruct the store with the reducer and the rehydrated state, but the dynamic reducer is not known when the store is constructed, so how do we transfer that knowledge from the server to the client?

populate the store before React rendering on the server side, mostly with responses from AJAX requests.

The standard way to use redux-dynostore is to attach the reducer when the React component is needed. This obviously causes a problem for the above use case. Part of the benefit is that we don't attach reducers if they are not needed due to conditional rendering.

That said, React is an optional part of redux-dynostore, so if you can identify a reducer will be required through some other mechanism (e.g. the result of a previous AJAX request), then you can call the attachReducers function directly on the store instance without relying on React rendering at all, e.g.

const store = createStore(staticReducers, dynostore(dynamicReducers()))

const promise1 = store.dispatch(makeRequest1())
const promise2 = promise1.then(data => {
  if (data.someCondition) {
    store.attachReducers({ dynamicReducer })
    return store.dispatch(makeRequest2())
  }
  return Promise.resolve()
})

Promise.all([promise1, promise2]).then(() => {
  // render app
})

You could still use a dynamic component that attaches the same reducer during rendering, but if you are controlling the reducer attachment this way then there isn't much value in it (it would actually do a bit of extra work it didn't need to).

Does any of this help?

klis87 commented 5 years ago

@mpeyper yeah, thx for the detailed answer.

The bit I'm unsure of is if the dynamic component will get a chance to attach again once it hits the client. I'm assuming the idea with Redux in SSR is that you just reconstruct the store with the reducer and the rehydrated state, but the dynamic reducer is not known when the store is constructed, so how do we transfer that knowledge from the server to the client?

Using this library state is still global, so I imagine to have a method on the server which would merge static state and dynamic state from dynamic reducers into one object. Then, on the client side, we could pass it in createStore, static reducers would just ignore this extra state, and dynamic ones could pick it on during mounting if a given dynamic slice was passed by the server.

Regarding 2nd problem, either we would need to have a configuration per route which dynamic slices are needed for a given route, or... to have a mechanism like in apollo to walk through all components to know which dynamic hocs are rendered, then fire necessary sagas, then render normally with already available state.

In the future it should be much easier because future React will allow to stop rendering, do sth, like run sagas, then continue rendering.

So I think that it is better to wait for this new React to come, then solving this problem will be much easier and SSR will be much more performant (comparing to apollo method)

jpeyper commented 5 years ago

Just FYI, I came across this discussion about using replaceReducers (how dynostore works) when SSR and some issues that it has in the react-redux v6 that we might need to watch out for.

https://github.com/reduxjs/react-redux/issues/1126

klis87 commented 5 years ago

@jpeyper interesting, let's hope they will solve it in react-redux, they have several ideas already. This is about reducers only though, dynamic sagas/epics which fetch data with requests are a different story, and I feel that with new React fetcher API we will be able to do SSR with only one render and without any extra configuration on the app level.

I am going to really spend some time on this issue in the next year, when new React will be there, for now I guess I will just code split only React components, I shouldn't have any issues fetching whole redux for all routes for now. But I think that splittable Redux compatible with SSR would be really so much beneficial, allowing scaling state layer of complex apps with maintaining single store concept.

If you prefer, you could close this issue for now, or just keep it opened for some time until circumstances of developing this feature will be better, I don't mind either way.

wilbert-abreu commented 5 years ago

Any updates on this issue?

jpeyper commented 5 years ago

Not from us, we're still not using SSR and don't plan to any time soon but we do have plans to support redux v6 soon (#20)

Not sure if @klis87 has tried out anything more now redux v6 is out now, or progressed further with SSR.

As always, if you're able and need the feature, we would love help solving this. PRs welcome.

klis87 commented 5 years ago

@jpeyper I didn't, because I really think that we need to wait for possibility to pause rendering on the server and resume, for example after a promise is resolved. Now it is not possible, that's why in many cases SSR involves double rendering (even 1st sometimes is not a full render like in react-apollo, but still)

For now I opted for an alternative solution to redux code splitting.Instead of code splitting, I created a library for managing remote data and remote data usually takes 90% of state. So because remote state is managed by one reducer, code splitting isn't needed so much anymore.

mpeyper commented 3 years ago

Closing. Please see #484 for details.