Nhost - Backend-as-a-Service with GraphQL for modern app development - Interview with Johan Eliasson
Hot Module Replacement (HMR) builds on top of the WDS. It enables an interface that makes it possible to swap modules live. For example, style-loader can update your CSS without forcing a refresh. Implementing HMR for styles is ideal because CSS is stateless by design.
HMR is possible with JavaScript too, but due to application state, it's harder. react-refresh-webpack-plugin and vue-hot-reload-api are good examples.
Given HMR can be complex to implement, a good compromise is to store application state to localStorage
and then hydrate the application based on that after a refresh. Doing this pushes the problem to the application side.
The following steps need to be enabled for HMR to work:
webpack.HotModuleReplacementPlugin
.module.hot.accept
and optionally module.hot.dispose
to clean module before replacing it.Using webpack-dev-server --hot
or running webpack-plugin-serve in hot
mode solves the first two problems. In this case, you have to handle only the last one yourself if you want to patch JavaScript application code. Skipping the --hot
flag and going through webpack configuration gives more flexibility.
The following listing contains the essential parts related to this approach. You will have to adapt from here to match your configuration style:
{
devServer: {
// Don't refresh if hot loading fails. Good while
// implementing the client interface.
hotOnly: true,
// If you want to refresh on errors too, set
// hot: true,
},
plugins: [
// Enable the plugin to let webpack communicate changes
// to WDS. --hot sets this automatically!
new webpack.HotModuleReplacementPlugin(),
],
}
Starting from webpack 5, there's an alternative tomodule.hot
.import.meta.webpackHot
has been designed with ES2015 modules and Node mjs file extension in mind as it doesn't allow mixing CommonJS and ES2015 syntax.
If you implement configuration like above without implementing the client interface, you will most likely end up with an error:
The message tells that even though the HMR interface notified the client portion of the code of a hot update, nothing was done about it and this is something to fix next.
The setup assumes you have setoptimization.moduleIds = 'named'
. If you run webpack indevelopment
mode, it will be on by default.
You should not enable HMR for your production configuration. It likely works, but it makes your bundles bigger than they should be.
If you are using Babel, configure it so that it lets webpack control module generation as otherwise, HMR logic won't work! See the Loading JavaScript chapter for the exact setup.
Webpack exposes the HMR interface through a global variable: module.hot
. It provides updates through module.hot.accept(<path to watch>, <handler>)
function and you need to patch the application there.
The following implementation illustrates the idea against the tutorial application:
src/index.js
import component from "./component";
let demoComponent = component();
document.body.appendChild(demoComponent);
// HMR interface
if (module.hot) {
// Capture hot update
module.hot.accept("./component", () => {
const nextComponent = component();
// Replace old content with the hot loaded one
document.body.replaceChild(nextComponent, demoComponent);
demoComponent = nextComponent;
});
}
If you refresh the browser, try to modify src/component.js
after this change, and alter the text to something else, you should notice that the browser does not refresh at all. Instead, it should replace the DOM node while retaining the rest of the application as is.
module.hot.accept
works with an array of filenames as well. The handler (second parameter) is optional.
The image below shows possible output:
The idea is the same with styling, React, Redux, and other technologies. Sometimes you don't have to implement the interface yourself even as available tooling takes care of that for you.
To prove that HMR retains application state, set up a checkbox based component next to the original. The module.hot.accept
code has to evolve to capture changes to it as well.
The if(module.hot)
block is eliminated entirely from the production build as minifier picks it up. The Minifying chapter delves deeper into this topic.
hot-accept-webpack-plugin and module-hot-accept-loader allow you to write if (module.hot) { module.hot.accept(); }
for each module that was matched. It's useful in case you have modules that should accept hot loading without implementing the patching behavior.
Deep dive into Hot Module Replacement by Stanimira Vlaeva discusses the topic in greater detail.
In the setup above, the WDS-related entries were injected automatically. Assuming you are using WDS through Node, you would have to set them yourself as the Node API doesn't support injecting. The example below illustrates how to achieve this:
entry: {
hmr: [
// Include the client code. Note host/post.
"webpack-dev-server/client?http://localhost:8080",
// Hot reload only when compiled successfully
"webpack/hot/only-dev-server",
// Alternative with refresh on failure
// "webpack/hot/dev-server",
],
...
},
Dynamic Loading through require.context
and HMR requires extra effort:
const req = require.context("./pages", true, /^(.*\.(jsx$))[^.]*$/g);
module.hot.accept(req.id, ...); // Replace modules here as above
HMR is one of those aspects of webpack that makes it attractive for developers and webpack has taken its implementation far. To work, HMR requires both client and server-side support. For this purpose, webpack-dev-server provides both. You will have to take care with the client-side, though, and either find a solution that implements the HMR interface or implement it yourself.
This book is available through Leanpub (digital), Amazon (paperback), and Kindle (digital). By purchasing the book you support the development of further content. A part of profit (~30%) goes to Tobias Koppers, the author of webpack.