Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to logically combine react-router and redux for client- and server-side rendering

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:

  1. Is my approach wrong because there is something I do not understand / know yet?
  2. Is it right to keep data loaded from REST API's in redux's store?
  3. Is'nt it a bit awkward to have components which keep state in the redux store and others managing their state by themselfs?
  4. Is the idea to have interaction -> URL change -> data fetching -> store update -> re-render simply wrong?

I am open for every kind of suggestion.

like image 688
Jason Nerer Avatar asked Aug 28 '15 14:08

Jason Nerer


1 Answers

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:

1. router.js

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));
    });
};

2. store.js

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.

3. rendering on the server

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 });
});

4. adapting the client

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]);
});

To answer your questions:

  1. Your approach seems right. We do the same. One could even include the url as part of the state.
  2. All state inside of the redux store is a good thing. This way you have one single source of truth.
  3. We are still working out what should go where right now. Currently we request the data on componentDidMount on the server it should already be there.
like image 146
David Langer Avatar answered Oct 20 '22 15:10

David Langer