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.babelrcwebpack.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 = appserver.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.
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.jsserver-- index.js-- server.js.babelrcwebpack.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.
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.jscommon-- App.jsserver-- index.js-- server.js.babelrcwebpack.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.
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
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!