Universal (Isomorphic) Rendering
React-redux-universal-hot-example is a hot boilerplate to provide universal rendering. It integrates webpack-isomorphic-tools to support React Redux rendering on both client and server sides.
As the slogen “All the modern best practices in one example” titled by this boilerplate, I dip into this framework to find the tricks in it. Followed by the features and questions I raised.
How to achieve Server-Side Rendering (SSR)?
In the traditional React programming, client only receives a html file without UI elements and a series of Javascript files. After that, client starts to render the UI elements. However, this may cause a rendering delay that is sometimes untolerable. Further, client renders nothing if the Javascript of browser is disabled.
Take React-boilerplate as an instance, it sends the html file directly while client get the web page:
app.get('*', (req, res) => res.sendFile(path.resolve(outputPath, 'index.html')));
To achieve server-side rendering, we can apply the ReactDOM.renderToString to render the first html file with an initial set of UI elements:
res.send('<!doctype html>\n' +
ReactDOM.renderToString(<Html
assets= {webpackIsomorphicTools.assets()}
component={component}
store={store}/>));
In which Html is a React component defined as:
In this case, server tries to render the Html component each time the client get the page, and return the whole UI-rendered html file to cilent. After that, client will start to render its own page and replace the server’s page.
How to pre-load data before Server-Side Rendering?
Since SSR only renders the first snapshot and immediately sends the html to client, that means some asynchronous actions cannot be completed and client will receive components which are not fully initialized. To delay the rendering of a component, the example applies Redux-async-connect to ensure the data is loaded before first rendering.
@asyncConnect([{
promise: ({store: {dispatch, getState}}) => {
const promises = []; if (!isInfoLoaded(getState())) {
promises.push(dispatch(loadInfo()));
}
if (!isAuthLoaded(getState())) {
promises.push(dispatch(loadAuth()));
} return Promise.all(promises);
}
}])
How to make store states consistent?
For states in a Redux store, the example make it consistent between server and client by puting the whole state map to window.__data in Html.js:
<body>
<div id="content" dangerouslySetInnerHTML={{__html: content}}/>
<script dangerouslySetInnerHTML={{__html: `window.__data=${serialize(store.getState())};`}} charSet="UTF-8"/>
<script src={assets.javascript.main} charSet="UTF-8"/>
</body>
And client will initialize its store by extracting the window.__data in client.js:
const store = createStore(_browserHistory, client, window.__data);
And this trick keeps the states consistent between server and client.
How to guarantee the rendered result consistent?
In this example, the client.js examines the checksum of the rendered html to guarantee the first result rendered by client is equal to the result from server:
if (!dest || !dest.firstChild || !dest.firstChild.attributes || !dest.firstChild.attributes['data-react-checksum']) {
console.error('Server-side React render was discarded. Make sure that your initial render does not contain any client-side code.');}
This approach ensures the rendered component is reusable by client and make no UI change after client’s rendering.
However, some third-party React components are not renderable in SSR, e.g., Carousel and Gridfy. Since these components use window to measure layout which is unavailable on server. We should avoid using these components until they are supported in universal rendering.
What is done by webpack-isomorphic-tools?
The isomorphic tool is the core of this example, it fixes the require() for assets on Node server with webpack plugin, and this makes SSR possible. The plugin generates webpack-assets.json and map the assets to webpack loader:
{
...
assets:
{
"./assets/images/husky.jpg":"/assets/esfs0fa6a254a6ebf2ad.jpg",
"./src/components/Header/Header.scss": {
"headerItems": "headerItems___2uGFX",
"headerNavBar": "headerNavBar___8vLWj",
"_style": "..."
}
}
}
This approach makes the source codes as individual modules, since the styles are suffixed by a hash so the different modules won’t share the styles with same name. However, this makes it hard to import styles from node_modules.
Support dynamic header
Since SSR renders the whole html dynamically, we can take advantage on it to insert dynamic header to the page. Helmet provides a solution to achieve this, in Html.js:
const head = Helmet.rewind();
return (
<html lang="en-us">
<head>
{head.base.toComponent()}
{head.title.toComponent()}
{head.meta.toComponent()}
{head.link.toComponent()}
{head.script.toComponent()}
...
</head>
...
</html>
and put the Helmet component in App.js (or other component):
<div>
<Helmet {...config.app.head}/>
{this.props.children}
</div>
Api server
React-redux-universal-hot-example supports a set of apis at 3030 port, which demonstrates how to send asynchronous Redux actions. I skip tracing this part since this implementation is not scalable for production.
Reference
React-redux-universal-hot-example: https://github.com/erikras/react-redux-universal-hot-example
webpack-isomorphic-tools: https://github.com/halt-hammerzeit/webpack-isomorphic-tools