Learn what Hot Module Replacement is and how it works by reinventing it from scratch!
🎥 Watch my talk at Remix Bay Area for a live demo of this tutorial!
teaser.mp4
This tutorial focuses on the core concepts of HMR. So instead of wrestling with the browser and network, we'll keep everything in Node. To that end, we'll use a simple CLI app meant to mirror a typical web app.
If you get stuck at any point, you can peek at the solutions.
Start by running the example app:
node ./run.cjs
Interactable elements will be highlighted red.
To change the name of your character, press n. A prompt should appear where you can enter in the new name. Press Enter to confirm.
To change your health, press + or -.
Open app/rolls.cjs
and create a new component.
It should let users press r to roll a random number between 1 and 6.
It should also display the last seven rolls.
Ok time to start reinventing HMR!
Start by watching the app directory for changes with chokidar. Whenever file changes are detected, re-require the app.
require
is caching all of the modules in our app, so we get the old app whenever we re-require it.
When changes are detected, invalidate all the modules from the app directory.
We're re-requiring the app, but nothing is happening. When changes are detected, let's signal to the browser that it should re-render the app.
Nice! You've reinvented live reload!
The whole point of HMR is to be surgical about picking up code changes so that state is preserved. Right now, we're invalidating the whole app, so we're losing state all over the place.
When changes are detected, invalidate only the modules that changes in the app directory. Make sure to also invalidate any direct or indirect dependents of the changed module! In other words, bubble up the invalidation.
HINT: to determine dependents, you can use esbuild:
let esbuild = require("esbuild")
let result = await esbuild.build({
bundle: true,
entryPoints: [entrypoint],
metafile: true,
write: false,
platform: "node",
logLevel: "silent",
})
// you'll need to use the metafile to determine dependents
let dependents = somehowGetDependents(result.metafile)
Nice! You've reinvented HMR!
You may have noticed that while most of the app state is preserved, the state for the module we are changing is lost. That's true even if we only change the UI component and not the state. For better developer experience, let's move the state into its own module. That way we can change the UI without losing state.