Skip to main content
Bun’s bundler supports Hot Module Replacement (HMR) for fast development iteration without losing application state.

What is HMR?

Hot Module Replacement (HMR) allows you to update modules in a running application without a full page reload. When you edit a file:
  1. Bun detects the change
  2. Rebuilds only the affected modules
  3. Sends the update to the browser
  4. The browser applies the update without refreshing
This preserves:
  • Application state (form inputs, scroll position, etc.)
  • React component state
  • Global variables
  • WebSocket connections

Watch mode

Enable watch mode to rebuild automatically on file changes:
bun build ./src/index.tsx --outdir ./dist --watch
This watches all files imported by your entry point and triggers a rebuild when any of them change.

Development server

For full HMR support, use Bun’s development server (coming soon):
dev-server.ts
const server = Bun.serve({
  port: 3000,
  
  async fetch(req) {
    const url = new URL(req.url);
    
    // Serve bundled assets with HMR
    if (url.pathname === "/app.js") {
      const result = await Bun.build({
        entrypoints: ["./src/index.tsx"],
        outdir: "./dist",
        watch: true,
      });
      
      return new Response(result.outputs[0]);
    }
    
    // Serve HTML
    return new Response(Bun.file("./index.html"));
  },
});

console.log(`Dev server running at http://localhost:${server.port}`);

React Fast Refresh

React Fast Refresh is automatically enabled for .jsx and .tsx files:
Counter.tsx
import { useState } from "react";

export function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}
When you edit this component:
  1. The component re-renders
  2. State is preserved (count value remains)
  3. No page reload occurs
Enable Fast Refresh explicitly:
bun build ./src/index.tsx --outdir ./dist --react-fast-refresh
Or via the JavaScript API:
await Bun.build({
  entrypoints: ["./src/index.tsx"],
  outdir: "./dist",
  reactFastRefresh: true,
});

Module types

Hot updates

Modules can opt into hot updates using the import.meta.hot API:
if (import.meta.hot) {
  import.meta.hot.accept(() => {
    // Module was updated
    console.log("Module reloaded!");
  });
}

Self-accepting modules

A module can handle its own updates:
data.ts
let data = loadData();

if (import.meta.hot) {
  import.meta.hot.accept(() => {
    // Reload data when this module updates
    data = loadData();
  });
}

export { data };

Accepting dependencies

A module can handle updates to its dependencies:
app.ts
import { config } from "./config";

if (import.meta.hot) {
  import.meta.hot.accept("./config", (newConfig) => {
    // Config was updated
    console.log("Config updated:", newConfig);
  });
}

CSS HMR

CSS updates are applied without a page reload:
styles.css
.button {
  background: blue;
  color: white;
}
When you change the background color:
styles.css
.button {
  background: red; /* Changed */
  color: white;
}
The style updates immediately in the browser without losing application state.

Image HMR

Images are also hot-reloaded:
import logo from "./logo.png";

function App() {
  return <img src={logo} alt="Logo" />;
}
When you replace logo.png, the new image appears without a page reload.

Configuration

Debouncing

Watch mode debounces file changes to avoid rebuilding too frequently:
await Bun.build({
  entrypoints: ["./src/index.tsx"],
  outdir: "./dist",
  watch: {
    // Wait 100ms after a file change before rebuilding
    debounce: 100,
  },
});

Ignored paths

Exclude paths from watch mode:
await Bun.build({
  entrypoints: ["./src/index.tsx"],
  outdir: "./dist",
  watch: {
    ignore: [
      "**/node_modules/**",
      "**/.git/**",
      "**/dist/**",
    ],
  },
});

Error handling

When a build error occurs:
  1. The error is displayed in the terminal
  2. The browser shows an error overlay (if using a dev server)
  3. The previous working code continues to run
  4. When you fix the error, HMR resumes

Performance

HMR in Bun is fast:
  • Incremental rebuilds (only changed modules)
  • Parallel processing
  • Minimal browser updates (only changed code)
  • No disk I/O for small changes

Examples

React application

App.tsx
import { useState } from "react";
import "./App.css";

export function App() {
  const [count, setCount] = useState(0);
  
  return (
    <div className="app">
      <h1>Counter: {count}</h1>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}
index.tsx
import { createRoot } from "react-dom/client";
import { App } from "./App";

const root = createRoot(document.getElementById("root")!);
root.render(<App />);
bun build ./src/index.tsx --outdir ./dist --watch --react-fast-refresh

Vanilla JavaScript

app.ts
import { render } from "./render";
import "./styles.css";

render();

if (import.meta.hot) {
  import.meta.hot.accept("./render", (newRender) => {
    newRender();
  });
}

State management

store.ts
class Store {
  state = { count: 0 };
  
  increment() {
    this.state.count++;
  }
}

const store = new Store();

if (import.meta.hot) {
  // Preserve store state across HMR updates
  if (import.meta.hot.data.store) {
    Object.assign(store, import.meta.hot.data.store);
  }
  
  import.meta.hot.dispose((data) => {
    data.store = store;
  });
}

export { store };

Limitations

  • HMR requires a development server (static files don’t support HMR)
  • Not all changes can be hot-reloaded (e.g., changing module exports)
  • Some state might be lost (e.g., closures, module-level variables)
  • Full page reload is needed for:
    • HTML changes
    • Adding/removing files
    • Changes to build configuration
    • Changes to imported npm packages

Best practices

Keep state in React

Store application state in React components rather than module-level variables:
// Good: State in component
export function Counter() {
  const [count, setCount] = useState(0);
  return <div>{count}</div>;
}

// Bad: Module-level state (lost on HMR)
let count = 0;
export function Counter() {
  return <div>{count}</div>;
}

Use HMR API for cleanup

Clean up side effects when a module is replaced:
const interval = setInterval(() => {
  console.log("Tick");
}, 1000);

if (import.meta.hot) {
  import.meta.hot.dispose(() => {
    clearInterval(interval);
  });
}

Organize for HMR

Split code into small modules that can be updated independently:
// Good: Small, focused modules
import { Button } from "./Button";
import { Input } from "./Input";

// Bad: Large modules that change frequently
import { Button, Input, Modal, Dropdown } from "./components";

Troubleshooting

Changes not detected

Make sure the file is imported by your entry point:
// Entry point must import changed files
import "./styles.css"; // This file will be watched

Full reload on every change

Check if your modules are self-accepting:
if (import.meta.hot) {
  import.meta.hot.accept(); // Add this
}

State lost on update

Use React Fast Refresh for components or implement state preservation:
if (import.meta.hot) {
  import.meta.hot.dispose((data) => {
    data.myState = currentState;
  });
  
  import.meta.hot.accept(() => {
    if (import.meta.hot.data.myState) {
      restoreState(import.meta.hot.data.myState);
    }
  });
}

Build docs developers (and LLMs) love