React: Server-side rendering and hot reloading

Digging into isomorphic/universal app development

Justin Jung
8 min readAug 3, 2016

UPDATE:
If you check out my git repo, I’ve updated the project with
Webpack v3 and React Router v4. So parts of this article are outdated, but they still describe the problems I faced and how I resolved them.

I’m about to share my journey on creating a boilerplate. If you know what Webpack, React and Redux are, and:

  • You want to know how to build an isomorphic/universal app, or
  • You want to know if it is worth to build an isomorphic/universal app, or
  • You are sick of copying & pasting hot reloading configs and don’t understand why it’s not working, or
  • You are sick of depending on other’s boilerplates without understanding them,

my story would definitely be helpful. For each step, you can find a link to the related version of source code. Link to a full repo is https://github.com/justinjung04/universal-boilerplate.

Introduction

Here’s my story. I have been using React for two years now. Front-end development has evolved around it, and Webpack + React + Redux (+ Hot reloading) has been a great boilerplate for web projects. I was completely satisfied with it.

Then I encountered few articles that discussed about an isomorphic/universal app.

Huh?

That was my first reaction. Why would I need extra stuff when my boilerplate is already awesome?

Your index.html has nothing but <div id=‘app’></div>. The search engine has no idea what your website contains.

Hmm, fair enough. How could I improve it then?

Instead of rendering everything from the client, render your page in the server and let the client take over from it.

Roadmap

To find out how hard it is to make my app isomorphic/universal, I created a roadmap to make a new boilerplate. It started off with a really basic one, which had the following modules:

  • React
  • Redux
  • Express
  • Webpack

And the following modules/features were to be added:

  • Server-side rendering
  • Hot-reloading
  • React-router
  • CSS-loader

The initial boilerplate was v1.0.0.

Problem: Let client take over?

Rendering a landing page from the server was easy. There is a method called renderToString in ReactDOMServer, which literally takes a ReactElement and render its initial html.

res.send(`
<!doctype html>
<html>
<head>
<title>My Universal App</title>
</head>
<body>
<div id='app'>${renderToString(<App />)}</div>
</body>
</html>
`);

Now that a full html with markups were created, I was supposed to bind events handlers… somehow.

Solution: React got your back.

Well, it turned out that it was too easy to be true! All I had to do was the following:

res.send(`
<!doctype html>
<html>
<head>
<title>My Universal App</title>
</head>
<body>
<div id='app'>${renderToString(<App />)}</div>
<script src='bundle.js'></script>
</body>
</html>
`)

The magic was done by React. Here’s a breakdown of what’s happening:

  1. The server initially renders markups with ReactDOMServer’s renderToString function.
  2. At this stage, the markups don’t have any event-handlers attached.
  3. Script bundle.js is loaded that tells the client to render.
  4. The client renders markups with ReactDOM’s render function — all markups have event-handlers attached.
  5. Client-rendered markups overwrite the div with id ‘app’.
  6. When overwritting, React sees that markups are already there.
  7. React preserves the markups and only attaches event-handlers.

How about window.__INITIAL_STATE__ for Redux?

That is one way of injecting initial states from the server to the client, suggested by Redux. Such injection was unnecessary for me, since my reducers from the client already had the initial states.

The boilerplate was updated to v1.1.0.

Problem: Hot reloading configuration?

Hot loading is critical for dev efficiency, so I wanted to at least understand it before I start editing my webpack config and babelrc. I spent decent amount of time researching, and I personally recommend Hot Reloading in React posted by Dan Abramov. It thoroughly explains difference between Webpack HMR (Hot Module Replacement), React-hot-loader and React-transform. To summarize it in my words,

  1. There are two distinct steps — re-bundling + re-rendering.
  2. Re-bundling is supported by Webpack HMR. It’s almost like polling, so when a file changes Webpack re-bundles and triggers a callback function where re-rendering can be handled.
  3. Webpack HMR is great, but it has a drawback. Re-rendering is done by re-loading the updated files, which does not preserve internal states of DOM elements.
  4. That’s where React-hot-loader and React-transform come in. They have different implementations to ‘proxy’ React elements. When a file is updated, the proxy elements point to the updated files while preserving the internal states.
  5. But remember. React-hot-loader and React-transform are only valuable if you need to save internal states of components. If you eliminate internal states by using Redux, you don’t have to use these modules!

Solution: Webpack HMR is good enough.

I could successfully verify that Webpack HMR is sufficient for hot reloading. Simply:

Enable Webpack HMR:

// webpack.config.dev.js
entry: [
'webpack-hot-middleware/client',
path.resolve(__dirname, 'src')
],
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin()

]
// server.js
app.use(require('webpack-hot-middleware')(compiler));

And add re-rendering logic:

import App from './app';render(<App />, document.getElementById('app'));if(process.env.NODE_ENV == 'development' && module.hot) {
module.hot.accept('./app', () => {
const NewApp = require('./app').default;
render(<NewApp />, document.getElementById('app'));
});
}

I could even apply it for reducers:

import reducers from './reducers';const store = createStore(reducers);if(process.env.NODE_ENV == 'development' && module.hot) {
module.hot.accept('./reducers', () => {
store.replaceReducer(require('./reducers').default);
});
}

Super simple, no extra modules needed. Great news is that this logic needs to be added to a root component only. A root component contains the entire app in it, so change in any component will be reflected in the root component. Thus, the code above says “If my app changes in any way, re-render the entire thing.”

Why would you want to re-render entire app when a small change is made?

It doesn’t actually re-render the entire app. Remember that we have React! It will compare the old and a new app, and only re-render the components that changed.

The boilerplate was updated to v1.2.0.

Problem: Server rendering with React-router.

Then I wanted to add routing with React-router. I tried changing my app.js as follows:

export default class App extends Component {
render() {
return (
<Provider store={store}>
<Router history={browserHistory}>
<Route path='/'>
<IndexRoute component={Home} />
<Route path='page' component={Page} />
</Route>
</Router>

</Provider>
);
}
}

And I got an error.

Warning: [react-router] `Router` no longer defaults the history prop to hash history. Please use the `hashHistory` singleton instead.

I figured that the error message is being caused on the server-side, and found this Server Rendering Guide from React-router as a solution.

Solution: RouterContext and match.

Basically, I needed a very specific configuration to render Router from the server. I simply followed the guide and restructured my code as follows:

// routes.js
export default (
<Route path='/'>
<IndexRoute component={Home} />
<Route path='page' component={Page} />
</Route>
);
// middleware.js
match({ routes, location: req.url },
(error, redirectLocation, renderProps) => {
if(renderProps) {
res.status(200).send(`
<!doctype html>
<html>
<head>
<title>My Universal App</title>
</head>
<body>
<div id='app'>${renderToString(
<Provider store={createStore(reducers)}>
<RouterContext {...renderProps} />
</Provider>
)}
</div>
<script src='bundle.js'></script>
</body>
</html>
`);
}

}
);
// index.js
render(
<Provider store={store}>
<Router history={history}>
{ routes }
</Router>
</Provider>
,
document.getElementById('app')
);

Boom. It worked like a magic — perhaps I should research deeper into this later.

The boilerplate was updated to v1.3.0.

Problem: React-router doesn’t hot-reload.

Afterwards, I faced another error from React-router when running on a dev server. Hot reloading was broken.

Warning: [react-router] You cannot change <Router routes>; it will be ignored

React-router didn’t allow routes to be updated, and as a consequence, the entire app was never hot-reloading! Too bad, but I needed a work-around.

Solution: Use React-transform to proxy.

After all, I had to put React-transform back in. I chose React-transform over React-hot-loader for its simplicity — I understand that it’s being replaced with React-hotlLoader 3, but React-transform was still good enough for my purpose. I updated webpack.config.js as follows:

loaders: [
{
test: /\.js$/,
loader: ‘babel’,
include: path.resolve(__dirname, ‘src’),
query: {
presets: [ ‘react-hmre’ ]
}

}
]

That’s it! With React elements having a proxy, React-router never knew when the components got updated. With Webpack HMR + React-transform, I finally had hot-reloading.

The boilerplate was updated to v1.4.0.

Problem: What about CSS?

This was a problem that I never imagined before. Since I already had hot-reloading setup, I thought it would be as easy as adding css loaders to webpack.config.js. No it wasn’t.

// webpack.config.dev.js + webpack.config.prod.js
loaders: [
{
test: /\.css/,
loader: ‘style!css’,
include: path.resolve(__dirname, ‘src’)
}
]
// home/index.js
import './index.css';

SyntaxError: …/home/index.css: Unexpected token (1:0)

After a struggle, I realized that it was coming from the server. When the server initially tried to render the app, it had no idea what css files are.

Solution: Use Extract-text-webpack-plugin.

Basically,

  1. The server can handle js files only.
  2. Webpack can handle both js and css files.

Based on these two facts, I thought of bundling the css files separately and making server-rendered html to refer to it. Fortunately, Extract-text-webpack-plugin did exactly what I described. I added the plugin as follows:

// webpack.config.dev.js + webpack.config.prod.js
plugins: [
new ExtractTextPlugin('bundle.css')
],
module: {
loaders: [
{
test: /\.css/,
loader: ExtractTextPlugin.extract('style', 'css'),
include: path.resolve(__dirname, 'src')
}
]
}
// middleware.js
res.status(200).send(`
<!doctype html>
<html>
<head>
<title>My Universal App</title>
<link rel='stylesheet' href='bundle.css'>
</head>
<body>
<div id='app'>${renderToString(
<Provider store={createStore(reducers)}>
<RouterContext {...renderProps} />
</Provider>
)}</div>
<script src='bundle.js'></script>
</body>
</html>
`);

After I successfully deligated Webpack to bundle css separately, the server needed a way to ignore css files when it tries initial rendering. I decided to use another process.env variable to check if the files are being bundled with Webpack or not.

// webpack.config.dev.js + webpack.config.prod.js
plugins: [
new webpack.DefinePlugin({
'process.env': {
WEBPACK: true
}
})

]
// .../home/index.js
if(process.env.WEBPACK) require('./index.css');

The boilerplate was updated to v1.5.0.

Problem: CSS doesn’t hot reload.

When I scraped all css files and bundled them, there was no way for them to be hot reloaded. Extract-text-webpack-plugin also described that hot reloading wasn’t supported yet.

Should I give up hot reloading? No. Instead, I could give up something else — server-side rendering in dev.

Solution: Exclude server-side rendering in dev.

Don’t get me wrong. My production environment still had server-side rendering. But I knew that it has no impact in development phase, thus I could safely exclude it to take advantage of hot reloading.

To do so, my code changed as follows:

// webpack.config.dev.js
module: {
loaders: [
{
test: /\.scss/,
loader: 'style!css',
include: path.resolve(__dirname, 'src')
}
]
}
// middleware.js
if(process.env.NODE_ENV == 'development') {
res.status(200).send(`
<!doctype html>
<html>
<head>
<title>My Universal App</title>
</head>
<body>
<div id='app'></div>
<script src='bundle.js'></script>
</body>
</html>
`);

} else if(process.env.NODE_ENV == 'production') {
res.send(`
<!doctype html>
<html>
<head>
<title>My Universal App</title>
<link rel='stylesheet' href='bundle.css'>
</head>
<body>
<div id='app'>${renderToString(
<Provider store={store}>
<App />
</Provider>
)}</div>
<script src='bundle.js'></script>
</body>
</html>
`);
}

The boilerplate was updated to its final version, v2.0.0.

Wrap-up

Server-side rendering is worth having. Although it wasn’t easy to setup the boilerplate, it does not add extra complexity to actual dev processes. Rather, it was a great opportunity for me to go deeper into web dev trends, and it will make my future websites search-engine optimized.

But web dev is changing fast and my post will be outdated soon. A take-away message from my post shouldn’t be a link to my repo, but the problems that I encountered and how the solutions wisely bypassed them.

--

--