Redux Server Side Rendering: Actions

1.4k views Asked by At

I'm building an universal app with React and Redux which renders on server-side via NodeJS and ExpressJS.

all is working fine, Express handlers call {match} from 'react-router' and a new store instance will be created each time.

The issue I'm having is: the {renderToString} from 'react-dom/server' renders only the pristine version of the store, if something changes the store (eg: action dispatched via componentWillMount) the store will be updated but the generated markup will not change until a new renderToString is called.

  1. I don't know how (per-request) the reducers will change the state, so, I can't provide an initial state before calling renderToString.
  2. I would like to avoid a further renderToString call.

this is my sample code:

const store = createStore(
  reducers,

  // an object that provides the initial generic state
  res.viewModel.__INITIAL_STATE__ || Object.create(null),

  middlewares
);

// now the store is pristine and calling store.getState()
// I will retrieve an invalid version of the state

const markup = renderToString(
  <Provider store={store}>
    {<RouterContext {...renderProps} />}
  </Provider>
);

// now the store is correctly computed and calling
// store.getState() gets the right version but the markup
// generated is still old. Only recalling `renderToString` will get 
// the right markup

const markup2 = renderToString(
  <Provider store={store}>
    {<RouterContext {...renderProps} />}
  </Provider>
);
2

There are 2 answers

3
Richard Scarrott On

One approach is to provide a per-route action as a static method on your Route components which would then be called by your express middleware before calling renderToString e.g.

class Index extends Component {
   componentDidMount() {
       // NOTE: The client fetches the data in `componentDidMount` instead of `componentWillMount` to ensure we don't duplicate efforts on the server.
       const { dispatch } = this.props;
       Index.fetchData({ dispatch });
   }

   render() {
       ...
   }
}

Index.fetchData = function({ dispatch }) {
    // Returns promise via redux-thunk / redux-promise
    return dispatch(fetchDataIfNeeded());
};

export default connect(state => state)(Index);

Then in your middleware you'd call all the static fetchData methods defined on the matched route components before rendering, e.g.

app.use((req, res) => {
    match({
        routes,
        location: req.url
    }, (error, redirectLocation, renderProps) => {
        const store = configureStore();
        const requests = renderProps.components
            .map(component => {
                // Handle `connect`ed components.
                if (component.WrappedComponent) {
                    component = component.WrappedComponent;
                }
                if (component.fetchData) {
                    return component.fetchData({
                        dispatch: store.dispatch
                    })
                    .catch(() => {});
                }
            });
        Promise.all(requests)
            .then(() => {
                // Store now contains all necessary state.
                const markup = renderToString(
                    <Provider store={store}>
                        <RouterContext {...renderProps} />
                    </Provider>
                );
                res.send(`<!doctype html><html><body>${markup}</body></html>`);
            });
    });
});

You can see a complete example here in the 60frames boilerplate.

Relevant modules:

https://github.com/60frames/react-boilerplate/blob/master/src/components/index/Index.js

https://github.com/60frames/react-boilerplate/blob/master/src/server.js

1
Hitmands On

Another found approach:

Using Redux-Saga

https://medium.com/@navgarcha7891/react-server-side-rendering-with-simple-redux-store-hydration-9f77ab66900a#.xin76jnk4

Pros

  1. Avoids specific server-side additional code
  2. Stays in the Redux Way

Cons

  1. Calls renderToString twice

example:

import { END } from 'redux-saga';

store.runSaga = sagaMiddleware.run;
store.close = () => store.dispatch(END);


// server.js
store.runSaga(sagas).done.then(() => {
    const state = store.getState();
    const html = renderToString(rootComponent);

    reply(renderApplication({state, html}));
});

renderToString(rootComponent);

store.close();