Hot module replacement
A changed module is patched into the running page instead of reloading it, so scroll position, open dialogs and half-filled forms survive a save. --no-hot opts out.
It is not tied to a framework. A module opts in with import.meta.hot:
import { render } from "./view.js"; render(); import.meta.hot?.accept();
If nothing on the way up accepts a change, the page reloads.
The API
accept() | Re-run this module in place |
accept(cb) | …and call cb(newExports) after |
accept(dep, cb) | Re-run that dependency and call cb(itsNewExports) |
accept([deps], cb) | The same, for several |
signal | AbortSignal, aborted just before this module is replaced |
keep(key, make) | Made once, returned on every replacement after |
dispose(cb) | Runs before replacement, with data |
data | An object that survives replacement |
decline() | Refuse replacement — any change reaching here reloads |
invalidate() | Hand the update up to this module's importers |
import.meta.hot is undefined in a production build, so if (import.meta.hot) is how a module says “dev only” and still typechecks.
Cleanup, without cleanup code
signal is aborted just before your module is replaced. Anything that takes one tears itself down:
addEventListener("resize", onResize, { signal: import.meta.hot?.signal });
In production the signal is undefined, so the listener stays registered. For APIs that do not take a signal, register cleanup only in a hot build:
const timer = setInterval(tick, 1000); if (import.meta.hot) { import.meta.hot.signal.addEventListener("abort", () => clearInterval(timer)); }
State across replacements
const cache = import.meta.hot ? import.meta.hot.keep("cache", () => new Map()) : new Map();
In a hot build, made once and returned on every replacement. In production, the module creates its own map.
Component refresh is a plugin
esdev implements no framework's scheme and knows the name of none. What it provides is the generic half, and it is the same for everyone:
import.meta.hotand the update channel, on in any hot loop;ctx.hot—truefor a browser build the loop is running hot, and only there;the compiler's component registrations, when a plugin asks for them.
A scheme is a plugin on top of that, declared with the project's other plugins:
{ "plugins": ["./plugins/otfw-refresh.mjs"], "build": { "targets": { "web": { "entry": "index.html", "outdir": "dist" } } } }
export default { name: "otfw-refresh", jsx: { refresh: true }, transform: { filter: { id: /\.[jt]sx$/ }, handler: (code, id, ctx) => (ctx.hot ? { code: wrapForHot(code, id) } : null), }, };
The refresh itself is an ordinary accept callback, so a scheme is a consumer of the hot API rather than something esdev knows about.
React
Already wired up in the React template: edit a component and it re-renders with its useState intact. It is React Fast Refresh, implemented by the template's plugins/react-refresh.mjs and listed in its esdev.json:
"plugins": ["./plugins/react-refresh.mjs"]
React has no privileges here: delete the plugin entry and the React template loses Fast Refresh like any other framework would.
Add refresh to an existing project
@opentf/esdev-plugin-react provides a reusable refresh plugin. Its initial release is not published yet. From an ES-Runtime checkout, run npm pack ./packages/esdev-plugin-react, then install the resulting tarball and react-refresh as development dependencies in your application:
npm install --save-dev /path/to/opentf-esdev-plugin-react-0.1.0.tgz react-refresh@^0.19.0
Add the package to the project's top-level plugin list:
{ "plugins": ["@opentf/esdev-plugin-react"], "jsx": { "importSource": "react" }, "build": { "targets": { "web": { "entry": "index.html", "outdir": "dist" } } } }
Run esdev start. The package initializes the refresh runtime itself, so the application entry needs no refresh bootstrap. When replacing an existing template plugin, remove its plugin entry and the template's refresh bootstrap import. The embedded React templates still use their local plugin.
Dev mode turns treeshaking off, so the React template's dev bundle is larger and rebuilds take longer. Development only — nothing you ship changes.
For the server restart, port selection and watch rules around HMR, see The dev loop.