Documentation Index
Fetch the complete documentation index at: https://mintlify.com/remix-run/react-router/llms.txt
Use this file to discover all available pages before exploring further.
Hot Module Replacement
Hot Module Replacement (HMR) enables you to see changes instantly during development without losing application state.
How HMR Works
When you edit a file:
- Vite detects the change
- Sends update to browser via WebSocket
- React Router applies changes
- UI updates without full page reload
- Application state is preserved
Framework Mode
HMR is automatic in framework mode:
pnpm dev
# ✓ HMR enabled
# ✓ React Fast Refresh enabled
Edit any route file and see instant updates:
// app/routes/_index.tsx
export default function Home() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Home Page</h1>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
</div>
);
}
// Edit the heading:
// <h1>Welcome Home</h1>
// → Updates instantly, count is preserved!
What Gets Updated
Components
Component changes apply immediately:
export function Component() {
return <div>Version 1</div>;
}
// Edit to:
export function Component() {
return <div>Version 2</div>;
}
// → Instantly updates
Loaders
Loader changes trigger revalidation:
export async function loader() {
return { message: "Hello" };
}
// Edit to:
export async function loader() {
return { message: "Hello World" };
}
// → Loader re-runs, data updates
Actions
Action changes are applied on next submission:
export async function action({ request }) {
// Old logic
return { success: true };
}
// Edit to:
export async function action({ request }) {
// New logic
return { success: true, timestamp: Date.now() };
}
// → Next form submission uses new logic
Styles
CSS updates without reload:
import "./styles.css";
export function Component() {
return <div className="container">Content</div>;
}
// Edit styles.css:
// .container { color: red; } → color: blue;
// → Color changes instantly
State Preservation
React Fast Refresh preserves component state:
export function Counter() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
<input
value={text}
onChange={(e) => setText(e.target.value)}
/>
</div>
);
}
// Edit the button text
// → count and text are preserved!
State Reset Cases
State resets when:
- Exporting non-components:
// This resets state
export const config = { theme: "dark" };
export function Component() {
const [state] = useState(0);
return <div>{state}</div>;
}
- Anonymous exports:
// This resets state
export default () => {
const [state] = useState(0);
return <div>{state}</div>;
};
// This preserves state
export default function Component() {
const [state] = useState(0);
return <div>{state}</div>;
}
- Class components:
// Class components always reset
export default class MyComponent extends React.Component {
state = { count: 0 };
render() {
return <div>{this.state.count}</div>;
}
}
Route Updates
Route configuration updates automatically:
// app/routes.ts
import { route } from "@react-router/dev/routes";
export default [
route("/", "./home.tsx"),
route("/about", "./about.tsx"),
];
// Add a route:
export default [
route("/", "./home.tsx"),
route("/about", "./about.tsx"),
route("/contact", "./contact.tsx"), // ← New route
];
// → New route immediately available
Loader Revalidation
Control when loaders revalidate:
// Revalidate on every edit
export async function loader() {
return { timestamp: Date.now() };
}
// Prevent revalidation during HMR
if (import.meta.hot) {
import.meta.hot.accept(() => {
// Don't revalidate
});
}
Custom HMR Handling
Manual HMR logic for special cases:
// app/routes/dashboard.tsx
export function Component() {
const [data, setData] = useState(initialData);
// Custom HMR handling
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
console.log("Dashboard updated");
// Custom update logic
if (newModule?.resetData) {
setData(newModule.initialData);
}
});
import.meta.hot.dispose(() => {
console.log("Dashboard disposed");
// Cleanup
});
}
return <div>{/* ... */}</div>;
}
Data Mode
Enable HMR in data mode:
import { createBrowserRouter } from "react-router";
const routes = [/* ... */];
const router = createBrowserRouter(routes);
// HMR for routes
if (import.meta.hot) {
import.meta.hot.accept("./routes", async () => {
const newRoutes = await import("./routes?t=" + Date.now());
router._internalSetRoutes(newRoutes.default);
});
}
Production Builds
HMR code is removed in production:
if (import.meta.hot) {
// This code is stripped out in production
import.meta.hot.accept();
}
No performance impact on production builds.
Debugging HMR
Enable HMR Logging
// vite.config.ts
export default defineConfig({
server: {
hmr: {
overlay: true, // Show errors in overlay
},
},
plugins: [
reactRouter({
hmr: {
// HMR options
},
}),
],
});
Check HMR Status
if (import.meta.hot) {
console.log("HMR is enabled");
import.meta.hot.on("vite:beforeUpdate", () => {
console.log("About to update");
});
import.meta.hot.on("vite:afterUpdate", () => {
console.log("Update complete");
});
}
Force Full Reload
Some changes require full reload:
if (import.meta.hot) {
import.meta.hot.accept(() => {
// Force full reload for this module
import.meta.hot.invalidate();
});
}
HMR Boundaries
Define update boundaries:
// utils/api.ts
export const API_URL = "/api";
if (import.meta.hot) {
// Changes to this file trigger full reload
import.meta.hot.decline();
}
// components/Chart.tsx
import * as d3 from "d3";
if (import.meta.hot) {
// Accept updates without propagating
import.meta.hot.accept();
}
Common Issues
HMR Not Working
Problem: Changes don’t update
Solutions:
- Check dev server is running
- Verify WebSocket connection
- Clear browser cache
- Restart dev server
# Restart with clean cache
pnpm dev --force
Duplicate Module Errors
Problem: “Module imported multiple times”
Solution: Use consistent import paths
// ❌ Bad - different paths
import { util } from "./util";
import { util2 } from "./util.ts";
// ✅ Good - consistent paths
import { util, util2 } from "./util";
State Loss
Problem: State resets on every change
Solution: Export named functions
// ❌ Anonymous - resets state
export default function() {
const [state] = useState(0);
return <div>{state}</div>;
}
// ✅ Named - preserves state
export default function Component() {
const [state] = useState(0);
return <div>{state}</div>;
}
Custom Server HMR
Enable HMR with custom servers:
// server.mjs
import express from "express";
import { createServer } from "vite";
const app = express();
const vite = await createServer({
server: { middlewareMode: true },
appType: "custom",
});
app.use(vite.middlewares);
// HMR endpoint
app.get("/__vite_hmr", (req, res) => {
vite.ws.send({
type: "custom",
event: "route-update",
data: { route: "/example" },
});
});
app.listen(3000);
Best Practices
- Use named exports: For better Fast Refresh
- Avoid side effects: In module scope
- Split large files: Improves HMR speed
- Use HMR for dev only: Don’t rely on it in production
- Test full reloads: Ensure app works without HMR
Optimize HMR Speed
// vite.config.ts
export default defineConfig({
optimizeDeps: {
include: [
"react",
"react-dom",
"react-router",
],
},
server: {
hmr: {
timeout: 30000,
},
},
});
Reduce Update Scope
// Extract static code
const CONSTANTS = {
API_URL: "/api",
TIMEOUT: 5000,
};
// Component updates independently
export function Component() {
return <div>{CONSTANTS.API_URL}</div>;
}
Framework Mode HMR
Framework mode includes enhanced HMR:
- Route-level updates
- Loader revalidation
- Asset updates
- CSS hot reload
- React Fast Refresh
All enabled by default with pnpm dev.