How to Set Up Hot Module Replacement (HMR) in Bun for Automatic Development Reloads
Bun provides built-in Hot Module Replacement that activates when you pass development: true and hmr: true to Bun.serve, using a WebSocket at /_bun/hmr to push updates and the import.meta.hot API to handle module swaps without full page reloads.
Setting up Hot Module Replacement (HMR) for development in the oven-sh/bun repository requires no external bundlers or plugins. Bun ships with a complete, integrated HMR system that coordinates between the server runtime and browser to deliver instant updates as you edit source files.
How Bun's HMR Architecture Works
Bun's HMR implementation consists of three coordinated components located in src/bake/. The server-side runtime (src/bake/hmr-runtime-server.ts) initializes a WebSocket endpoint at /_bun/hmr and registers the registerUpdate callback to listen for file changes. When the bundler emits a hot update, the server invokes replaceModules from src/bake/hmr-module.ts to merge new module definitions into the registry. The client-side runtime (src/bake/hmr-runtime-client.ts) maintains a WebSocket connection, receives update payloads, and injects new script tags to execute the hot swap.
Configuring the Development Server
To enable automatic reloads, configure Bun.serve with the development and hmr options set to true. According to the type definitions in src/bake/bake.d.ts, these flags instruct the runtime to enable dev-mode diagnostics and embed the HMR client runtime into the bundle.
// server.ts
import { serve } from "bun";
serve({
development: true, // Required for HMR diagnostics
hmr: true, // Enables Hot Module Replacement
port: 3000,
fetch(req) {
return new Response("Hello World");
},
});
Executing bun run server.ts starts the development environment. The runtime automatically bundles your code with HMR support and opens the WebSocket channel for pushing updates to connected browsers.
Using the import.meta.hot API
The HMR runtime exposes a hot context via import.meta.hot that allows modules to participate in the update process. This API is implemented in src/bake/hmr-module.ts and provides methods for accepting updates, cleaning up resources, and persisting state across reloads.
Self-Accepting Modules
The simplest pattern involves a module accepting its own replacement. When the file changes, Bun executes the accept callback, allowing you to re-render components or re-initialize logic without losing application state.
// components/Counter.tsx
export function Counter() { /* ... */ }
if (import.meta.hot) {
import.meta.hot.accept(() => {
console.log("Counter updated - re-render if needed");
});
}
If a module does not register an accept handler, Bun safely falls back to a full page reload by calling fullReload(), which emits the bun:beforeFullReload event before executing location.reload().
Accepting Dependency Updates
Modules can handle updates from specific imports by passing a module specifier to accept. The callback receives the updated module namespace, allowing you to replace local references.
// pages/Home.tsx
import { Header } from "./Header";
export function Home() { /* ... */ }
if (import.meta.hot) {
import.meta.hot.accept("./Header", (newHeader) => {
// Replace the imported binding with the new module
Header = newHeader.Header;
});
}
Cleanup with Dispose Handlers
Before a module is replaced, Bun invokes any registered dispose handlers to clean up timers, event listeners, or subscriptions. The import.meta.hot.data object persists between disposes, enabling you to maintain state across reloads.
if (import.meta.hot) {
import.meta.hot.dispose(() => {
clearInterval(pollingTimer);
subscription.unsubscribe();
});
}
Server-Side Update Flow
When a source file changes, the server-side logic in src/bake/hmr-runtime-server.ts triggers the bundler to generate a hot update chunk. The system calls replaceModules(modules, sourceMapId) from src/bake/hmr-module.ts, which:
- Merges new module definitions into
unloadedModuleRegistry - Walks the module graph to locate HMR boundaries
- Executes dispose callbacks on modules being replaced
- Triggers
refreshRuntime.performReactRefresh()if React Fast Refresh is loaded - Signals the client to perform a
fullReload()if the update cannot be safely applied
Client-Side Update Delivery
Upon page load, the client runtime in src/bake/hmr-runtime-client.ts opens a WebSocket connection to /_bun/hmr and listens for MessageId.hot_update payloads. When an update arrives, the client:
- Parses the payload using
DataViewReader - Updates CSS via
editCssArrayoreditCssContent - Creates a new
<script>tag with a Blob URL containing the updated JavaScript - Invokes the global
bun:hmrfunction once the script loads, which callsreplaceModuleson the client-side registry - Executes any
import.meta.hot.acceptcallbacks to complete the hot swap
Complete Working Example
The following setup demonstrates a React application with HMR enabled. The entry point accepts updates from the root component, while the development server handles bundling and static file serving.
// main.tsx
import React from "React";
import { createRoot } from "react-dom/client";
import App from "./App";
const root = createRoot(document.getElementById("root")!);
root.render(<App />);
if (import.meta.hot) {
import.meta.hot.accept("./App", (mod) => {
root.render(<mod.App />);
});
}
// dev-server.ts
import { serve } from "bun";
serve({
development: true,
hmr: true,
port: 3000,
async fetch(req) {
return await Bun.serveStatic(req, {
directory: "./dist",
});
},
});
Run bun run dev-server.ts to start the environment. Editing App.tsx will instantly update the browser without a full page reload, preserving application state through the import.meta.hot lifecycle.
Summary
- Enable HMR by setting
development: trueandhmr: truein yourBun.serveconfiguration according tosrc/bake/bake.d.ts. - The runtime coordinates updates through three core files:
hmr-runtime-server.ts,hmr-runtime-client.ts, andhmr-module.ts. - Use
import.meta.hot.accept()to handle module replacements without reloading the page. - Implement
import.meta.hot.dispose()for cleanup and useimport.meta.hot.datato persist state across reloads. - Bun automatically falls back to
fullReload()when a module cannot be safely hot-replaced.
Frequently Asked Questions
Does Bun require Webpack or Vite to use HMR?
No. Bun includes a built-in HMR system that activates when you enable the hmr: true flag in your server configuration. The runtime handles bundling, WebSocket communication via /_bun/hmr, and module replacement internally without requiring external build tools.
Why does my browser perform a full page reload instead of a hot update?
Bun performs a full page reload when a changed module lacks an import.meta.hot.accept handler or when a runtime error occurs during the update process. This behavior is implemented in src/bake/hmr-runtime-client.ts via the fullReload() function and ensures application stability when hot replacement is not possible.
How do I preserve state during hot reloads in Bun?
Store state in the import.meta.hot.data object. This object survives the dispose cycle and is passed to the new module instance, allowing you to restore previous values after import.meta.hot.accept triggers. This mechanism is managed by the module registry in src/bake/hmr-module.ts.
Can I use Bun HMR with frameworks other than React?
Yes. While Bun automatically detects React and enables Fast Refresh via refreshRuntime.performReactRefresh(), the import.meta.hot API is framework-agnostic. You can use accept and dispose callbacks to manually trigger re-renders or re-initialization in Vue, Svelte, or vanilla JavaScript applications.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →