Martin Haagensli

@martinhaagensli

Hot reload all the things!

February 24th 2017

How to use Webpack to achieve Hot Module Replacement on the back and front-end for a more productive development environment.

Part I: Server

Setting up our webpack configuration

First we’re going to need to install Webpack and a few more dependencies;

yarn add webpack babel-loader babel-core babel-preset-env webpack-node-externals start-server-webpack-plugin

Then let’s create our .babelrc;

{
"presets": [["env", {"modules": false}]]
}

Now we’re going to set up our webpack config for the server in webpack.config.server.js;

const webpack = require('webpack')
const path = require('path')
const nodeExternals = require('webpack-node-externals')
const StartServerPlugin = require('start-server-webpack-plugin')
module.exports = {
entry: [
'webpack/hot/poll?1000',
'./server/index'
],
watch: true,
target: 'node',
externals: [nodeExternals({
whitelist: ['webpack/hot/poll?1000']
})],
module: {
rules: [{
test: /\.js?$/,
use: 'babel-loader',
exclude: /node_modules/
}]
},
plugins: [
new StartServerPlugin('server.js'),
new webpack.NamedModulesPlugin(),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
new webpack.DefinePlugin({
"process.env": {
"BUILD_TARGET": JSON.stringify('server')
}
}),
],
output: {
path: path.join(__dirname, '.build'),
filename: 'server.js'
}
}

Now, create a folder called “server” with two files in it; index.js and server.js.
The index.js file will server as our mounting point and server.js will be our actual application. Our project structure should look like this;

server
-- index.js
-- server.js
.babelrc
webpack.config.server.babel.js

Let’s install and set up express;

yarn add express

Then we create our server application in /server/server.js;

import express from 'express'
const app = express()
app.get('/api', (req, res) => {
res.send({
message: 'I am a server route and can also be hot reloaded!'
})
})
export default app

And then in /server/index.js let’s add the magic sauce to get HMR working with our Express application;

import http from 'http'
import app from './server'
const server = http.createServer(app)
let currentApp = app
server.listen(3000)
if (module.hot) {
module.hot.accept('./server', () => {
server.removeListener('request', currentApp)
server.on('request', app)
currentApp = app
})
}

In our index file Webpack is polling for changes to our server.js file. On changes to the file we reattach the the listener from Express to our import. Thanks to Webpack 2 we don’t need to re-require our server.js file, but it’s important that we accept the same file that’s being imported for Hot Module Replacement.

Let’s try it out. Add this to our package.json scripts;

"scripts": {
"start:server": "rm -rf ./build && webpack --config webpack.config.server.js"
}

Now run;

npm run start:server

Open your browser and go to http://localhost:3000/api
You should see a message saying “I am a server route and I can also be hot reloaded!”. Then try to change that message in the server file and refresh the page. The message should now have changed. Notice in your terminal that the server itself doesn’t restart but Webpack updates the modules through HMR. We’re now hot reloading your Express application thanks to Webpack!

In the next part we will add server-side rendered React with HMR on both the server and the client.

Part II: React with HMR on the server and client

On the server

Let’s start by adding React to our dependencies;

yarn add react react-dom babel-preset-react

Then add the react preset to .babelrc;

{
"presets": [["env", {"modules": false}], "react"]
}

Next, let’s create a folder to hold our components, let’s call it “common”.
Create a file called App.js inside. 
Your folder structure should now look like this;

common
-- App.js
server
-- index.js
-- server.js
.babelrc
webpack.config.server.babel.js

Let’s create our React component in App.js;

import React from 'react'
const App = () => <div>Hello from React!</div>
export default App

Finally let’s render our component on the server, change /server/server.js to this;

import express from 'express'
import React from 'react'
import { renderToString } from 'react-dom/server'
import App from '../common/App'
const app = express()
app.get('/api', (req, res) => {
res.send({
message: 'I am a server route and can also be hot reloaded!'
})
})
app.get('*', (req,res) => {
let application = renderToString(<App />)
    let html = `<!doctype html>
<html class="no-js" lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>HMR all the things!</title>
<meta name="description" content="">
<meta name="viewport"
content="width=device-width, initial-scale=1">
</head>
<body>
<div id="root">${application}</div>
</body>
</html>`
res.send(html)
})
export default app

Now run

npm run start:server

and go to http://localhost:3000/ and you will see our server rendered React component. Try editing the component in /common/App.js and refresh the page and it should update. We now have server rendered React with Hot Module Replacement in addition to HMR on our regular server routes.

Client side React — the final piece of the puzzle

Let’s install a few more dependencies for our client;

yarn add webpack-dev-server react-hot-loader@next npm-run-all

First, let’s create our client side webpack configuration in webpack.config.client.js;

const webpack = require('webpack')
const path = require('path')
module.exports = {
devtool: 'inline-source-map',
entry: [
'react-hot-loader/patch',
'webpack-dev-server/client?http://localhost:3001',
'webpack/hot/only-dev-server',
'./client/index'
],
target: 'web',
module: {
rules: [{
test: /\.js?$/,
use: 'babel-loader',
include: [
path.join(__dirname, 'client'),
path.join(__dirname, 'common')
]
}]
},
plugins: [
new webpack.NamedModulesPlugin(),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
new webpack.DefinePlugin({
"process.env": {
"BUILD_TARGET": JSON.stringify("client")
}
})
],
devServer: {
host: 'localhost',
port: 3001,
historyApiFallback: true,
hot: true
},
output: {
path: path.join(__dirname, '.build'),
publicPath: 'http://localhost:3001/',
filename: 'client.js'
}
}

Then create a folder named “client” and an index.js file inside;

import React from 'react'
import { render } from 'react-dom'
import { AppContainer } from 'react-hot-loader'
import App from '../common/App'
render(<AppContainer>
<App />
</AppContainer>, document.getElementById('root'))
if (module.hot) {
module.hot.accept('../common/App', () => {
render(<AppContainer>
<App />
</AppContainer>, document.getElementById('root'))
})
}

Our final folder structure should look like this;

client
-- index.js
common
-- App.js
server
-- index.js
-- server.js
.babelrc
webpack.config.server.babel.js

Then let’s add our client side script to the server rendered html in /server/server.js by adding <script src=”http://localhost:3001/client.js"></script> in our body tag;

let html = `<!doctype html>
<html class="no-js" lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>HMR all the things!</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id="root">${application}</div>
<script src="http://localhost:3001/client.js"></script>
</body>
</html>`

Lastly, change the scripts in package.json to:

"scripts": {
"start:server": "rm -rf ./build && webpack --config webpack.config.server.js",
"start:client": "webpack-dev-server --config webpack.config.client.js",
"start": "rm -rf ./.build && npm-run-all --parallel start:server start:client"
}

Now you can run;

npm start

and go to http://localhost:3000. We now how HMR working with our server side routes, as well as server and client side React.

Quickstart

Clone the repository here for a simple boilerplate;
https://github.com/mhaagens/hot-reload-all-the-things

Follow me on Twitter for more about front end development;
https://twitter.com/mhaagens

Acknowledgements

A huge thanks to Sean T. Larkin, Tobias Koppers and the rest of the Webpack team for the awesome work they do and for inspiring and sharing their knowledge with the community!

Hacker Noon is how hackers start their afternoons. We’re a part of the @AMI family. We are now accepting submissions and happy to discuss advertising & sponsorship opportunities.
If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!

More by Martin Haagensli

More Related Stories