Hot Reloading is a tricky thing. The configuration is very fragile and only senior webpack developer can set it up. That's why there're a lot of guides and example repos with working setup. We've encountered a little bit exotic case: Hot Reloading for React application in monorepo for both client and server-side code. Example repos brought us to this quest.
The best way to hot-reload universal React application is to use the webpack Hot Module Replacement (HMR) API for server-side code and React-Hot-Loader (RHL) for client entry. The client configuration is pretty simple. Just follow instructions from RHL repo. To reload server bundle on the fly look here.
We bumped into the issue with our initial approach for hot-reloading server-side. It was done via re-requiring server build result on each source file change and updating Koa middleware handler with the received module. Specifically, we used require-from-string
package from NPM to do that.
This is how it worked step by step (code is simplified a bit):
Create Koa (or some other node.js) server that will serve your application.
// Application middleware pointer that will be updated on each build
let serverMiddleware
const app = new Koa().use((ctx: Koa.Context, next: Koa.Middleware) => {
serverMiddleware(ctx, next)
)
Create server webpack compiler.
const serverCompiler = webpack(serverWebpackConfig)
Start webpack watch
mode. Calling the watch
method triggers the webpack runner, but then watches for changes (much like CLI: webpack --watch
)
const { outputPath, outputFileSystem: memoryFs } = serverCompiler
compilerServer.watch({}, (error: Error, serverStats: Stats) => {
// Build location example: serverStats.toJson().assetsByChunkName.main
const serverPath = getServerPath(serverStats, outputPath);
const serverBuffer = memoryFs.readFileSync(filename)
// Delete previous version of the server module from cache
delete require.cache[require.resolve(serverPath)]
const updatedServerMiddleware = requireFromString(
buffer.toString(),
filename
).default
// Point `serverMiddleware` to the updated server module
serverMiddleware = updatedServerMiddleware
});
In watch
callback, receive path to a fresh server build, created by webpack after some source file change.
Read this file using memoryFs
into a buffer
.
Get the updated server module using require-from-string
.
Use this module as the main application middleware in your server.
Webpack watch
handler will be executed on each source-file change resulting in hot-loading of the updated server module. Boom! We don't need to completely restart the server on each change to get the updated behavior. Quite simple, right?
This approach worked flawlessly while we had a small application. But in a couple of months, we noticed that the server started crashing in development mode after some running time. It looked like an issue with poor integration of react-loadable
from our side first, because there were React warnings in the console about updating state in an unmounted component. It was inconvenient, but not the end of the world. Once in an hour, we needed to do manually restart the server. But a month later it got much worse. After 5-8 edits server crashed — literally every 5 minutes. We couldn't ignore the issue anymore.
We started tackling the issue by debugging memory leaks in the whole application without thoughts specifically about hot-reload. We used Chrome Devtools to debug memory allocation. Specifically, JS heap snapshots were very helpful.
First, start application in debug mode and open Chrome Devtools attached to it. Type about:inspect
in Chrome url and click inspect
under your target app.
node --inspect-brk start-dev-server.js
Navigate to the Profiles tab in opened Devtools, and click the "Take Snapshot" button of the profiling type panel.