I'd like my React based SPA to render on server side (who's not these days). Therefore I want to combine React with react-router, redux and some build layer like isomorphic starterkit.
There is hapi universal redux which joins all together, but I am struggling with how to organize my flow. My data is coming from multiple endpoints of a REST API. Different components have different data needs and should load data just in time on the client. On the server instead, all data for a specific route (set of components) has to be fetched, and the necessary components rendered to strings.
In my first approach I used redux's middleware to create async actions, which load the data, return a promise, and trigger a SOME_DATA_ARRIVED
action when the promise resolves. Reducers then update my store, components re-render, all good. In principle, this works. But then I realized, that the flow becomes awkward, in the moment routing comes into play.
Some component that lists a number of data records has multiple links to filter the records. Every filtered data set should be available via it's own URL like /filter-by/:filter
. So I use different <Link to={...}>
components to change the URL on click and trigger the router. The router should update the store then according to the state represented by the current URL, which in turn causes a re-render of the relevant component.
That is not easy to achive. I tried componentWillUpdate
first to trigger an action, which asynchronously loaded my data, populated the store and caused another re-render cycle for my component. But this does not work on the server, since only 3 lifecycle methods are supported.
So I am looking for the right way to organize this. User interactions with the app that change the apps state from the users perspective should update the URL. IMO this should make the router somehow load the necessary data, update the store, and start the reconciliation process.
So interaction -> URL change -> data fetching -> store update -> re-render
.
This approach should work on the server also, since from the requested URL one should be able to determine the data to be loaded, generate initial state
and pass that state into the store
generation of redux. But I do not find a way to properly do that. So for me the following questions arise:
store
?state
in the redux store
and others managing their state
by themselfs?interaction -> URL change -> data fetching -> store update -> re-render
simply wrong?I am open for every kind of suggestion.
I did set up exactly the same thing today. What we already had, was a react-router and redux. We modularized some modules to inject things into them – and viola – it works. I used https://github.com/erikras/react-redux-universal-hot-example as a reference.
The parts:
We return a function (location, history, store)
to set up the router using promises. routes
is the route definition for the react-router containing all your components.
module.exports = function (location, history, store) {
return new Bluebird((resolve, reject) => {
Router.run(routes, location, (Handler, state) => {
const HandlerConnected = connect(_.identity)(Handler);
const component = (
<Provider store={store}>
{() => <HandlerConnected />}
</Provider>
);
resolve(component);
}).catch(console.error.bind(console));
});
};
You just pass the initial state to createStore(reducer, initialState)
. You just do this on the server and on the client. For the client you should make the state available via a script tag (ie. window.__initialstate__
).
See http://rackt.github.io/redux/docs/recipes/ServerRendering.html for more information.
Get your data, set up the initial state with that data (...data
). createRouter
= router.js
from above. res.render
is express rendering a jade template with the following
script.
window.csvistate.__initialstate__=!{initialState ? JSON.stringify(initialState) : 'null'};
...
#react-start
!= html
var initialState = { ...data };
var store = createStore(reducer, initialState);
createRouter(req.url, null, store).then(function (component) {
var html = React.renderToString(component);
res.render('community/neighbourhood', { html: html, initialState: initialState });
});
Your client can then do basically the same thing. location
could be HistoryLocation
from React-Router
const initialState = window.csvistate.__initialstate__;
const store = require('./store')(initialState);
router(location, null, store).then(component => {
React.render(component, document.getElementsByClassName('jsx-community-bulletinboard')[0]);
});
componentDidMount
on the server it should already be there.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With