Hot reloading in React apps with code splitting
Breaking hot reloading, and fixing it, sort of
After I joined Zoover in March, one of the first things we did was adding hot reloading to our React app, by using Webpack’s Hot Module Reloading and
react-hot-loader. Before that, we used
gulp-livereload and its full-page refresh to get the updated UI. Initially it was working quite well, but it got more unreliable along the way, and after we added code splitting and
react-loadable to our application, it got to the point where my colleagues were just doing a full-page refresh manually.
This is a pretty bad place to be in: not having confidence in the correctness of the hot update basically kills all advantages it might have: first, you’ll wait until you see the screen update; then, you’ll refresh anyway because you need to be sure that what you saw is correct. Frankly, we were better off with the full-page refresh that at least was automated. This had some drawbacks though: our server-side render setup means two Webpack builds, >1s rebuild times, and a server that is down when the full-page refresh occurs, which often meant it would take 5–10s to see changes on your screen.
The first guess was that code splitting, in combination with
react-loadable broke it.
In case you don’t know what those two things are: code splitting is the ability to define split points in your code. A split point is a point where Webpack splits up your bundle into separate files, that can be loaded on-demand. This could help you keep the weight of your critical assets down, and it’s an absolute necessity for bigger applications, especially on mobile (where bundle size hurts the most).
react-loadable is a pretty nifty React component that helps you import and render lazily-loaded components, and make it all work on the server as well (which is non-trivial).
That first guess initially seemed wrong: the thing that broke hot reloading in some (or many) cases was one of the React optimization plugins we used (turning those plugins on in development mode was probably not the best choice, but alas). They modified components in a way that made it difficult for
react-hot-loader to work. After turning off those plugins, hot reloading certainly worked better, but it still was broken for lazily-loaded components. Initially I thought it had something to do with the module not being required because of the
react-loadable works, but it turns out it (also) breaks at Webpack’s level.
For those unfamiliar with hot reloading in a Webpack and React context: you have to “catch” hot updates for a specific module in the same place where you render your application. If you get a hot update, you’ll need to re-render the application. Meanwhile,
react-hot-loader adds a small piece of code to every component module that registers that component with
react-hot-loader. Then, when you re-render your application,
React.createElement calls, and replaces React components with those from its own registry, which will have the latest versions from the hot update.
Now, if you’d expect updates from split modules to bubble up to the entry point, you would have the same expectations in life as I would have. And we would both be wrong. Turns out those updates only bubble up to the top module in the chunk of the changed module, so we’ll never know when to re-render.
There are a couple of options to make it work:
- Explicitly accept updates for all chunks in your entry point, and re-render for those updates.
- Use the presence of
process.env !== 'production'to add explicit, static
requirestatements for lazily-loaded modules.
However, both options essentially require double administration. I’m lazy, so I don’t want to do things more than once (I need to save time to compensate for all the Blue Screen of Deaths my XPS is giving me). What I want to do is turn off Webpack’s code-splitting in development mode. Sounds pretty easy, right? Well, turns out that’s more difficult than I thought.
webpackMode: eager could solve our problems, but we can only set it in a comment, on a per-import basis, not for everything at once. Adding
LimitChunkCountPlugin and setting
maxChunks: 1 disables the output of multiple files, but doesn’t seem to solve anything. After some hair pulling I figured there might be a compromise: maybe we can add the explicit require statements, automatically?
In come Babel plugins: the perfect place to parse source code and automatically add some statements, preventing the double administration we’re trying to avoid. It’s quite hard to find documentation on how to write a Babel plugin, other than looking at babel-handbook and some example plugins, but it’s really easy to get started with once you start up AST explorer.
Here’s the gist of the plugin I wrote: it simply adds static imports for every dynamic import it encounters, and replaces dynamic import calls with a Promise that immediately resolves with the default export of the imported module. When Webpack finally gets to this code, it doesn’t see dynamic import statements anymore, it doesn’t create any additional chunks, and hot-reloading works again.
Try it out for yourself:
To use it, simply add the plugin to your
Also, just to be sure: don’t use it in production mode, unless you want to disable code splitting over there as well.
Zoover is a travel website, located in Amsterdam. Our goal is to help our users find the best vacation possible. Our website is built with React, on a Node backend, and we’re always looking for people who are good at what they do.