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.

TLDR;

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.

Initial Hot Reloading approach

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):

  1. 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)
    )
    
  2. Create server webpack compiler.

    const serverCompiler = webpack(serverWebpackConfig)
    
  3. 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
    });
    
  4. In watch callback, receive path to a fresh server build, created by webpack after some source file change.

  5. Read this file using memoryFs into a buffer.

  6. Get the updated server module using require-from-string.

  7. 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?

Strange memory leaks

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.

Memory leaks in other libraries

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.

  1. 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
    
  2. Navigate to the Profiles tab in opened Devtools, and click the "Take Snapshot" button of the profiling type panel.

    https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7071c1de-1a1f-4024-ae4e-e632ae2e8734/chrome-1.png