HMR & React Refresh
Hot Module Replacement (HMR) allows updating individual modules without a full page reload, preserving application state.
End-to-end flow
Editor (keystroke)
│
▼
EditorFS.write()
│ detects change, debounces
▼
Worker receives "watch-update" message
│ with ContentChange[] (path, type, content)
▼
IncrementalBundler.rebuild(changes)
│ 1. Invalidate changed module caches
│ 2. Re-transform only changed files
│ 3. Walk any new local deps
│ 4. Fetch any new npm packages
│ 5. Clean up orphaned modules
│ 6. Emit full bundle (for fallback)
│ 7. Build HmrUpdate with per-module code
▼
Worker posts "hmr-update" to parent
│ updatedModules, removedModules, bundle (fallback)
▼
App broadcasts to iframe via postMessage
▼
Iframe HMR runtime receives "hmr-update"
│ 1. Find accept boundaries (walk reverse deps)
│ 2. Run dispose callbacks
│ 3. Replace module factories
│ 4. Delete removed modules
│ 5. Re-execute boundary modules
│ 6. React Refresh: performReactRefresh()
▼
UI updates without page reloadHMR runtime
The HMR runtime (hmr-runtime.ts) is a CommonJS module loader with module.hot API support:
module.hot.accept(cb?)- marks the module as an accept boundarymodule.hot.dispose(cb)- registers a cleanup callback that runs before the module is replacedmodule.hot.decline()- forces a full reload when this module changes
Accept boundaries
When a module changes, the runtime walks the reverse dependency graph to find the nearest module with module.hot.accept(). If found, only modules between the changed module and the boundary are re-executed. If no boundary is found, a full reload is triggered.
React Refresh automatically adds module.hot.accept() to every .tsx/.jsx file, making each component its own accept boundary.
React Refresh integration
When hmr.reactRefresh is enabled:
- The
reactRefreshTransformerwraps each component with$RefreshReg$/$RefreshSig$calls - Each component file gets a
module.hot.accept()postamble - The HMR runtime initializes the React Refresh runtime before executing the entry module
- After each HMR update,
performReactRefresh()tells React to re-render with updated component definitions
This preserves component state (e.g. useState values) across updates.
Cache clearing order
All module caches in modulesToReExecute are cleared in a first pass before any modules are re-executed in a second pass. This prevents an ordering bug where a parent module could re-execute and require() a dependency before that dependency's cache was cleared, getting stale exports.
Per-module source maps in HMR
Each module in an HMR update includes its own inline source map and //# sourceURL= annotation. The iframe's HMR listener extracts these and registers them with the source map resolver so runtime errors in HMR-updated modules resolve to correct original positions.