6.5 · difficulty 3/4 · 14 min read
How Frameworks Implement HMR
Source-level mechanics of React Fast Refresh (OXC implementation) and Vue HMR — how frameworks use Vite's HMR API to perform component-level hot updates without losing state.
Division of Responsibilities
Vite's HMR handles transport: detecting file changes, determining affected modules, and notifying the browser to reload them.
Framework HMR handles application: given new component code, how to apply it to the running UI without unmounting the entire component tree.
File changes
│
▼
Vite: determine affected modules, send WebSocket message
│
▼
Framework HMR runtime: receive message, perform component hot swap
│
├── React: Fast Refresh → update component function, preserve state
└── Vue: HMR API → re-render SFC, preserve data/setup stateReact Fast Refresh: OXC Implementation
Vite 8's @vitejs/plugin-react v6 implements React Fast Refresh using OXC (no more Babel):
What the plugin does
// @vitejs/plugin-react v6 processing of Button.tsx (simplified)
// Your code:
export function Button({ label }) {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(c => c+1)}>{label} {count}</button>
}
// After plugin injection (simplified):
import RefreshRuntime from "/@react-refresh"
export function Button({ label }) {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(c => c+1)}>{label} {count}</button>
}
// Register this component for Fast Refresh
RefreshRuntime.register(Button, "Button")
if (import.meta.hot) {
// Self-accept
import.meta.hot.accept(() => {
RefreshRuntime.performReactRefresh()
})
}Fast Refresh rules
Fast Refresh is not unconditional. State is preserved only when:
// ✅ State preserved: only render logic changed
export function Button({ label }) {
const [count, setCount] = useState(0)
return <button style={{ color: "red" }}>{label} {count}</button>
// ↑ color changed, but state is preserved
}
// ❌ State lost: hooks order or count changed
export function Button({ label }) {
const [count, setCount] = useState(0)
const [extra, setExtra] = useState("") // ← new hook added, state resets
return <button>{label}</button>
}
// ❌ State lost: file exports non-component content
export const API_URL = "https://..." // ← non-component export, whole module refreshes
export function Button() { /* ... */ }Practical rule: don't mix non-component code into React component files. If a file has both components and constants, split them into separate files.
Differences between v6 and v5
v5 (@vitejs/plugin-react):
- Uses Babel for transform
- babel-plugin-react-refresh injects HMR code
- Requires Babel-related packages
v6 (@vitejs/plugin-react):
- Uses OXC (Rust) for transform
- OXC has built-in React refresh transform
- No Babel needed, faster startup
- API is identical to v5 (vite.config.ts usage unchanged)Vue HMR: Hot Updates for SFCs
Vue SFC (Single File Component) HMR is implemented by @vitejs/plugin-vue:
<!-- Button.vue -->
<template>
<button :style="{ color }">{{ count }}</button>
</template>
<script setup>
import { ref } from "vue"
const count = ref(0)
const color = ref("red")
</script>After compilation, plugin-vue injects HMR code:
// Compiled output (simplified)
import { defineComponent, ref } from "vue"
import { createHotContext } from "/@vite/client"
const __hmrId__ = "Button.vue"
const __hot__ = createHotContext(__hmrId__)
// Component options
const Button = defineComponent({
setup() {
const count = ref(0)
const color = ref("red")
return { count, color }
},
// ...
})
// Vue HMR API
if (__hot__) {
__hot__.accept((newModule) => {
// Hot update: re-register the component
if (newModule) {
__hot__.data.Button = newModule.default
}
})
}Vue's HMR runtime can preserve ref and reactive values — as long as the component structure has not changed in a breaking way.
When a Full Page Reload Still Occurs
Even with framework HMR in place, certain situations still trigger a full page reload:
| Reason | When it occurs |
|---|---|
| No HMR boundary | No accept() found anywhere up the module graph |
| Route config changed | Route file modified, cannot hot-update, must reload |
| Main entry modified | src/main.tsx changed |
| Breaking component structure | Hooks count/order changed (React) |
| Plugin explicitly triggers | server.hot.send({ type: "full-reload" }) |
Self-check
- How does React Fast Refresh update component code without losing state?
- Why does defining non-component constants in a React component file cause HMR to fail? How do you fix it?
- What is the core difference between
@vitejs/plugin-reactv6 and v5? What impact does it have on developers? - When the
<style>block of a Vue SFC changes, does it trigger a full component HMR or only a CSS update?
// Analyze the HMR behavior of this React file under each modification scenario below:
// src/counter.tsx
const THRESHOLD = 100 // ← constant
export const formatCount = (n: number) => `${n} times` // ← utility function
export function Counter() {
const [count, setCount] = useState(0)
const [name, setName] = useState("default") // ← second state
return (
<div>
<p>{formatCount(count)}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
)
}
// Analyze the HMR behavior of each modification:
// A: Change the text format inside <p> ("times" → "count")
// B: Delete `const [name, setName] = useState("default")`
// C: Change the value of THRESHOLD
// D: Delete formatCount and move it to another file
// E: Add a third useState to Counter
// For each case:
// 1. Will HMR succeed?
// 2. Will the value of count be preserved?
// 3. If HMR fails, what happens instead?