Webpack Hot Reloading and React

December 29th, 2015 | hot reloading, javascript, react, webpack |

This post is an extract of a github repo I’m working on chtefi/react-stack-step-by-step to explain step-by-step from scratch a full reactjs stack

– You already use HR ?

If you have a project using webpack, React, you probably have followed some tutorials and blogs to add the Hot Reloading piece.

Here, we are going to explain what’s going on under the hood, and how the HR projects / plugins you installed are working together (webpack HR middlewares, react-transform..).

We are going to use the latest React HR project in date react-transform-hmr and not the older one react-hot-loader which has been sentenced to the maximum penalty : the-death-of-react-hot-loader. I guess it’s still working properly but you know that the Javascript community is always on the bleeding edge side!

– You don’t already use HR ?

If the HR part does not sound familiar to you, that’s fine because I’m going to explain step-by-step why it’s mandatory to use HR nowadays, and what is the way to install and use it into your project.

 

Let’s just start by resuming why everybody is now talking about HR (beyond Javascript).

Why should we enjoy Hot Reloading ?

In one word : productivity.
In another word : DX (Developer eXperience).

Some more explanations :

– you broke your F5 key years ago, you can’t refresh anymore
– you don’t want to develop blindly, then refresh, then try again. It’s a backend thing. We have a UI, it’s alive
– you don’t want to lose your application state (by doing a full refresh) if you just fixed a typo or a color
– that can display your compilation and runtime errors directly in the browser in the exact spot in the UI where it’s used
– you can do HR with Javascript, CSS, anything. We’ll just focus on React components here.
– no caveat

As soon as you put it in place, it will work forever without any modifications, for any of the future files you’re going to add. It’s just some pipes to plug together to make it work and last forever.

2015-12-28: actually there is only one caveat (!) : the React HR plugin we are going to use does not handle the stateless functional components created with the simpler React v0.14 syntax : const App = (props) => {

Hello {props.name}
};.
Refer to comment.

Now, let’s tackle the packages we need to install to use it in a nodejs project.

What packages to install to use HR ?

Let’s suppose you already have a base project using :

webpack to compile your javascript bundle(s)
React for your frontend ofc
– a nodejs server that runs a http server such as expressjs to serve your static content (html, js, css..) while you’re developing

You now want to experiment HR.

webpack to the rescue

webpack is actually the main actor dealing with the HR, it already exposes an API to process some part of the HR pipeline.

We just need to add some wrapper around to use its API, and some more logic to manage the specific React components state : you don’t want to lose the current state of your components when you change something in a js file (a style, some constant, a prop, add a React component inside an existing one etc.).

We need to install 4 packages : 2 for webpack, 2 for React.

webpack-dev-middleware
– a classic expressjs middleware, where requests are passed on
– it automatically watches the sources for changes to recompile the javascript bundle server side if some javascript source have changed
– it always serves the bundle up to date

webpack-hot-middleware
– a classic expressjs middleware, where requests are passed on
– it automatically subscribes to the bundle recompilation events (such as “start”, “done”), to notify the frontend that something has changed and it need to update itself
– it uses SSE to communicate with the frontend

Specific packages for React HR

babel-plugin-react-transform
– it can add any code around React component methods during the Babel compilation ES6+JSX to ES5. You should already have configured your babel loader in webpack, such as :

 module: {
    loaders: [{
      test: /\.js$/,
      loader: 'babel',
      include: path.join(__dirname, 'src'),
    }]
  },

react-transform-hmr
– it is used by babel-plugin-react-transform to add specific code around the React components to properly handle HR and their current state

That gives us :

$ npm install --save-dev webpack-dev-middleware
$ npm install --save-dev webpack-hot-middleware
$ npm install --save-dev babel-plugin-react-transform@beta
$ npm install --save-dev react-transform-hmr
// or for the copy/paste, all together :
$ npm i -D webpack-dev-middleware webpack-hot-middleware babel-plugin-react-transform@beta react-transform-hmr

2015-12-28: we explicitely ask for the beta (>=2.0.0) of babel-plugin-react-transform because for now, the latest published version does not work with Babel 6. But work has been done and is just waiting to be merged.

 

Now that we have installed the necessary packages, it’s time to configure them.

Configure Babel

We need to configure Babel (we are going to use .babelrc) to use babel-plugin-react-transform and react-transform-hmr to add the HR code around them.

The most basic configuration is :

{
  "presets": ["react", "es2015"],
  "env": {
    "development": {
      "plugins": [
        ["react-transform", {
          "transforms": [{
            "transform": "react-transform-hmr",
            "imports": ["react"],
            "locals": ["module"]
          }]
        }]
      ]
    }
  }
}

Basically :
– that adds the transform babel-plugin-react-transform for the development NODE_ENV only
– this transform retrieve all the React components it can find in the source code
– it passes them down to each of its processors defined in "transforms" to let them add their custom code. (in our case, react-transform-hmr will add the HR code)

 

For the record, babel-plugin-react-transform handles as many "transforms" as we want. They are just going to be called right after each other. For instance :

{
  "presets": ["react", "es2015"],
  "env": {
    "development": {
      "plugins": [
        ["react-transform", {
          "transforms": [{
            "transform": "react-transform-hmr",
            "imports": ["react"],
            "locals": ["module"]
          }, {
            "transform": "react-transform-catch-errors",
            "imports": ["react", "redbox-react"]
          }]
        }]
      ]
    }
  }
}

The React component code will pass through react-transform-hmr, then through react-transform-catch-errors.
Each of them will add its code around each components.

FYI, the latter is used to catch errors that are thrown in the render() method of the React components. Then it’s using its "imports" property to redirect the error to a visual React component. Here redbox-react displays a big red screen of the death with the stacktrace for instance. But it could be anything else.

Basically, react-transform-catch-errors just adds try { render() } catch (e) { ... } around the original render() method of your components, and in the catch, it’s returning the React component you gave in "imports". Makes sense right ?

Now, our React code is ready to handle HR.
We now have to make the server communicate to the browser that the code has changed and that it needs to update.

Handle server/client communication to send/receive HR updates

Bundle recompilation on the fly

First, we need to make the server aware that the source code has changed to recompile the bundle, and then notify the browser.
That’s the role of webpack-dev-middleware and webpack-hot-middleware.

webpack-dev-middleware will automatically start to watch the source code for changes and recompile the bundle
webpack-hot-middleware will be notified a new bundle is compiled and will notify the browser

We just need to plug them into expressjs as middlewares to start them :

var express = require('express');
var webpack = require('webpack');
var path = require('path');

var webpackDevMiddleware = require("webpack-dev-middleware");
var webpackHotMiddleware = require("webpack-hot-middleware");
var webpackConfig = require('../webpack.config');

var app = express();

var compiler = webpack(webpackConfig);
app.use(webpackDevMiddleware(compiler));
app.use(webpackHotMiddleware(compiler));
app.use(express.static('src'));

app.listen(3000);

But how is the browser going to handle the updates ? It’s where webpack itself rises.

Browser live update

We need to add some code client-side to deal with it, otherwise, it’s not possible. HR is not browser native or anything.

Therefore, to inject some more code, we will use the webpack bundle entry point in webpack.config.js, to add another entry.
A bundle could have several entry points.
It is just there to say to webpack : “hey, resolve the import dependency (for make the bundle) tree starting from those files!”.

entry: [
  'webpack-hot-middleware/client',
  path.join(__dirname, 'src', 'App.js'),
],

The entrypoint webpack-hot-middleware/client simply refers to the file node_modules/webpack-hot-middleware/client.js.
It is the file that contains the code that will be used in the browser to handle the SSE communication with the server (to intercept the update notifications).

Then we need to add a specific webpack internal plugin HotModuleReplacementPlugin to expose the generic webpack HR API in the browser :

plugins: [
  new webpack.optimize.OccurenceOrderPlugin(), // recommanded by webpack
  new webpack.HotModuleReplacementPlugin(),
  new webpack.NoErrorsPlugin() // recommanded by webpack
]

This API will be used by the code injected from webpack-hot-middleware/client when this one will retrieve some “update” events through its SSE. (specifically, it will call module.hot.apply(..) from HotModuleReplacementPlugin).
You follow ?

 

Nothing more to do, you’re good to go !

See it in action

Process :
– start your nodejs server
– go to your page using the bundle on your browser
– go to your code editor and change some javascript bits
– see the live update !
– if not, check your console in the browser to see some nice error messages

Behind the scene :
– the server automatically recompile the bundle if some javascript code is updated and notify the browser to update itself via SSE
– the bundle used in the browser contains some SSE code to intercept those notifications, some generic HR code to “patch” the javascript, and some custom HR code for the React components to not lose their current state

A boilerplate on github

You can checkout this boilerplate from @dan_abramov https://github.com/gaearon/react-transform-boilerplate to give a try.
It’s very simple and does exactly what we just talked about.
You will see some more options used but nothing fancy.

Its .babelrc is using react-hmre instead of our two transforms packages (babel-plugin-react-transform@beta and react-transform-hmr), but it is exactly the same actually. react-hmre simply encapsulates them.

More bits to learn

Let’s explain some more in-depth some aspects of the code used.

webpack-dev-middleware is optional, but…

Without webpack-dev-middleware, you just need to launch the webpack watch yourself :

// app.use(webpackDevMiddleware(compiler));
// replace the dev-middleware with a simple watch() (args are mandatory)
compiler.watch({}, function(){});
app.use(webpackHotMiddleware(compiler));

Because webpackHotMiddleware subscribes to the bundle compilation events (no matter what started it), it will work.

But you’ll suffer from some consequences : a bunch of .js and .json files in your project will appears each time a compilation occurs.

They contain the delta sent to the client to update itself (webpack only sends the updated chunks, not the whole bundle each time). The advantage of using webpack-dev-middleware is that you won’t see those files. They will be handled in-memory by itself. That’s why you need to install this particular package too.

babel-plugin-react-transform and react-transform-hmr

Without the code added by react-transform-hmr, webpack would not be able to hot update the React components, you would get this in the browser console :

[HMR] bundle rebuilding
[HMR] bundle rebuilt in 160ms
[HMR] Checking for updates on the server...
[HMR] The following modules couldn't be hot updated: (Full reload needed)
[HMR]  - ./src/App.js

What’s inside the SSE ?

1. The browser initializes a SSE request : (thanks to the code webpack-hot-middleware/client.js injected in the bundle) on this specific url : GET localhost:3000/__webpack_hmr (it never returns). It’s handled by the server that knows it’s SSE. (by the webpack-hot-middleware expressjs middleware)

Then if a Javascript file is edited, because webpack-dev-middleware started a watch on the sources, webpack-hot-middleware is notified (because it subscribed to the recompilation events) and notify the frontend via SSE with a new module map (used by webpack), such as :

data: {"action":"building"}

// few ms later ...

data: {"action":"built","time":260,"hash":"6b625811aa23ea1ec259","warnings":[],"errors":[],"modules":{"0":"multi main","1":"./~/fbjs/lib/invariant.js","2":"./~/react/lib/Object.assign.js","3":"./~/fbjs/lib/warning.js","4":"./~/fbjs/lib/ExecutionEnvironment.js","5":"./~/react/lib/ReactMount.js","6":"./~/react/lib/ReactElement.js", ...

Then the frontend asks for those 2 files:
(I guess the hash used in the GET comes from the SSE data ? I didn’t check)

GET localhost:3000/0.0119cbdcd4c2cf8d27c2.hot-update.js

{"h":"6b625811aa23ea1ec259","c":[0]}

GET localhost:3000/0119cbdcd4c2cf8d27c2.hot-update.json

webpackHotUpdate(0,{

/***/ 97:
/***/ function(module, exports, __webpack_require__) {

  /* WEBPACK VAR INJECTION */(function(module) {'use strict';

  ...

Those particular urls are served by webpack-dev-middleware which is keeping those files (*.hot-update.js[on]) in memory and serves them as any classic static file. They are requested by the code injected by webpack.HotModuleReplacementPlugin() that handles the responses and hot-updates the javascript with the new code.

Conclusion

Before I dug into, I was finding that the HR was a bit complicated to understand how it works, who are the actors, how does it communicate, etc. I was never sure what package was used for what exactly, and when. I hope those explanations were clear enough and that now, you know as much as me ! (or more!) Don’t hesitate to add more details or correct me if I’m wrong.

Finally, it’s not that complex but it just need a lot of “pipes” to plug everything together. But it all makes sense.

Unfortunately, there are still some caveats with the way I exposed :
– it doesn’t work for functional stateless React components const App = (props) => {

Hello {props.name}
};
babel-plugin-react-transform is simply an experiment, maybe it will die
– is working on to handle HR at the function level directly, to make it more generic
webpack is an awesome tool

Using react-hot-loader with a webpack-dev-server and a node server

May 14th, 2015 | expressjs, hot reloading, nodejs, react, webpack |

2015-12-31: You should check out my recent post to use the new way of doing HR with webpack, and to have way more details :
Webpack Hot Reloading and React : how ?

Don’t work without hot-reloading

I got a project running its own Node server instance, using expressjs. It’s already bundling its assets with webpack, and it’s using React with the nice ES6 features, all good.

But it lacks of hot reloading. :-(

This is something we must have nowadays when you’re working with webapps and CSS/Javascript. You don’t want to refresh your whole page and lose your current state each time, you are going to lose your time, your patience, and your productivity !

Leave express alone and add webpack-dev-server on top

Thus, I decided to take a look and tried to add it while keeping the express server instance up and working. I didn’t want to replace the whole thing by a webpack-dev-server and reconfigure the public assets paths and all other stuff that my expressjs was providing.

After some tests, some readings here and there, I got a pretty straightforward solution without using any custom proxy code, nor changing my generated html.

The solution

For reference:
– the expressjs server is running on localhost:3000
– the webpack-dev-server is going to run on localhost:3001

Here is a git diff of my changes:

packages.json

+ "react-hot-loader": "^1.2.7",
+ "webpack-dev-server": "^1.8.2"

Just the necessary packages to do the hot reloading. (npm install --save-dev)

webpack.config.js

+var webpack = require('webpack');

- entry: path.resolve(__dirname, '../src/client.js'),
+ entry: [
+   'webpack-dev-server/client?http://0.0.0.0:3001',
+   'webpack/hot/only-dev-server',
+   path.resolve(__dirname, '../src/client.js')
+ ],

modules: {
  loaders: [
    {
-     loader: 'babel-loader'
+     loaders: [ 'react-hot-loader', 'babel-loader' ]
    }
  ]
},

+ plugins: [
+   new webpack.HotModuleReplacementPlugin(),
+   new webpack.NoErrorsPlugin()
+ ]

– we add webpack-dev-server entries into entry
– we add the react-hot-loader loader when processing the .js (it will add some js to magically do the hot reloading)
– we add the plugins for hot reloading.

More details here http://gaearon.github.io/react-hot-loader/getstarted/

server.js

This is the most important part, where we define a new webpack-dev-server instance that will be listen by the browser to process the hot reloading if any js file change.

// existing express server
var app = express();
app.use(...)
app.get(...)
app.listen(3000);

+// we start a webpack-dev-server with our config
+var webpack = require('webpack');
+var WebpackDevServer = require('webpack-dev-server');
+var config = require('./webpack.config.js');

+new WebpackDevServer(webpack(config), {
+   hot: true,
+   historyApiFallback: true,
+   proxy: {
+     "*": "http://localhost:3000"
+   }
+}).listen(3001, 'localhost', function (err, result) {
+   if (err) {
+     console.log(err);
+   }
+
+   console.log('Listening at localhost:3001');
+});

We are only adding a new webpack-dev-server instance without altering at all our expressjs server listening on localhost:3000.

– our webpack-dev-server listens to localhost:3001 and handle the hotness
– every requests (“*”) are redirected to localhost:3000.

This server serves only as a proxy for the hot reloading. More details here http://webpack.github.io/docs/webpack-dev-server.html.

At the end, there is still no hot reloading on http://localhost:3000 (it’s still a pure expressjs server).

But if you browse http://localhost:3001, and more precisely http://localhost:3001/webpack-dev-server/, you will the famous “App ready” hot reloading toolbar and everything will be automatically updated if you change one of your React component.