Vite · Part 5 — HMR Deep Dive
How Hot Module Replacement swaps a single module without losing state: the import.meta.hot API, HMR boundaries and propagation, why some edits trigger a full reload, and framework Fast Refresh. With an HMR boundary lab.
The magic moment with Vite is editing a component and seeing the change appear while your form stays filled and your counter keeps its value {Khoảnh khắc kỳ diệu với Vite là sửa một component và thấy thay đổi xuất hiện trong khi form vẫn điền và counter giữ giá trị}. That’s Hot Module Replacement, and it’s not magic — it’s a small, learnable API {Đó là Hot Module Replacement, và không phải phép thuật — là một API nhỏ học được}.
Build up some counter state, then edit different modules to feel HMR vs a full reload {Tích lũy state counter, rồi sửa các module khác nhau để cảm nhận HMR vs reload đầy đủ}:
1. HMR vs live reload {HMR vs live reload}
Old tools did live reload: file changes → refresh the whole page → all state gone {Công cụ cũ dùng live reload: file đổi → refresh cả trang → mất hết state}. HMR is surgical: it replaces only the changed module in the running app, leaving everything else — and your state — intact {HMR là phẫu thuật: nó chỉ thay module đã đổi trong app đang chạy, để mọi thứ khác — và state — nguyên vẹn}.
The dev server pushes updates to the browser over a WebSocket {Dev server đẩy cập nhật tới trình duyệt qua WebSocket}. When you save, Vite figures out exactly which module changed and sends just that {Khi bạn lưu, Vite xác định chính xác module nào đổi và chỉ gửi cái đó}.
2. import.meta.hot — the API {import.meta.hot — API}
A module opts into HMR through import.meta.hot (only defined in dev) {Một module tham gia HMR qua import.meta.hot (chỉ có khi dev)}:
export const add = (a: number, b: number) => a + b;
if (import.meta.hot) {
// "I can handle my own updates — don't reload the page"
import.meta.hot.accept((newModule) => {
console.log("new version:", newModule?.add);
});
// clean up side effects before the old version is discarded
import.meta.hot.dispose(() => {
/* clear timers, remove listeners */
});
}
| Method {Phương thức} | Purpose {Mục đích} |
|---|---|
accept() | accept self-updates without reloading {chấp nhận tự cập nhật, không reload} |
accept(dep, cb) | accept updates from a specific imported module {chấp nhận cập nhật từ module import cụ thể} |
dispose(cb) | clean up before replacement {dọn dẹp trước khi thay} |
invalidate() | ”I can’t handle this” → bubble up the boundary {“tôi không xử lý được” → đẩy lên boundary} |
data | persist state across updates of the same module {giữ state qua các cập nhật của cùng module} |
3. HMR boundaries & propagation {Ranh giới HMR & lan truyền}
The key concept: a module that calls accept() is an HMR boundary {Khái niệm then chốt: module gọi accept() là một ranh giới HMR}. When a file changes {Khi một file đổi}:
- If the module self-accepts, swap it in place. Done {Nếu module tự chấp nhận, thay tại chỗ. Xong}.
- If not, the update propagates up to importers, looking for one that accepts it {Nếu không, cập nhật lan lên tới các importer, tìm cái chấp nhận}.
- If it reaches the entry with no accepting boundary, Vite falls back to a full page reload {Nếu tới entry mà không có boundary chấp nhận, Vite rơi về reload trang đầy đủ}.
This is why editing a leaf component hot-updates, but editing a shared utils.ts that nothing accepts triggers a reload {Đây là vì sao sửa component lá thì hot-update, nhưng sửa utils.ts dùng chung mà không ai chấp nhận thì reload}. The demo shows all three paths {Demo cho thấy cả ba đường}.
4. You rarely write this yourself {Bạn hiếm khi tự viết}
Here’s the good news: framework plugins wire HMR up for you {Tin tốt: plugin framework lo HMR cho bạn}.
- React →
@vitejs/plugin-reactenables Fast Refresh: edit a component and it re-renders with hook state preserved {bật Fast Refresh: sửa component và nó re-render với state hook được giữ}. - Vue →
@vitejs/plugin-vuedoes the same for SFCs {làm tương tự cho SFC}. - CSS → always hot-swaps with zero reload (CSS is inherently side-effect-free to replace) {luôn hot-swap không reload (CSS vốn không tác dụng phụ khi thay)}.
You only touch import.meta.hot directly for non-framework modules with side effects — a store, a global event bus, a manual chart instance {Bạn chỉ chạm import.meta.hot trực tiếp cho module ngoài framework có tác dụng phụ — một store, event bus toàn cục, một instance chart thủ công}.
5. Fast Refresh gotchas {Lưu ý Fast Refresh}
Fast Refresh preserves state only if the file exports React components cleanly {Fast Refresh giữ state chỉ khi file xuất component React gọn gàng}. It bails to a reload if {Nó rơi về reload nếu}:
- A file mixes a component export with non-component exports (move constants/utils to their own file) {File trộn export component với export không-component (chuyển hằng/util sang file riêng)}.
- An anonymous default export is used (
export default () => ...) — name your components {Dùng default export ẩn danh — đặt tên cho component}.
These rules are why “keep one component per file, named” makes HMR more reliable {Các luật này là vì sao “một component mỗi file, có tên” làm HMR đáng tin hơn}.
6. Exercises {Bài tập}
1. What’s the difference between live reload and HMR, and why does HMR keep your form state? {Khác biệt giữa live reload và HMR, và vì sao HMR giữ state form?}
Solution {Lời giải}
Live reload refreshes the whole page (state lost). HMR replaces only the changed module in the running app, so unrelated state survives {Live reload refresh cả trang (mất state). HMR chỉ thay module đã đổi trong app đang chạy, nên state không liên quan sống sót}.
2. You edit a shared utils.ts imported by many files and the whole page reloads instead of hot-updating. Why? {Bạn sửa utils.ts dùng chung được nhiều file import và cả trang reload thay vì hot-update. Vì sao?}
Solution {Lời giải}
No module in the import chain called accept() for it, so the update propagated to the entry with no boundary → full reload {Không module nào trong chuỗi import gọi accept() cho nó, nên cập nhật lan tới entry không có boundary → reload đầy đủ}.
3. Your React component loses its state on every edit even though Fast Refresh is on. What’s a likely cause? {Component React của bạn mất state mỗi lần sửa dù Fast Refresh đang bật. Nguyên nhân có khả năng?}
Solution {Lời giải}
The file mixes component and non-component exports, or uses an anonymous default export — both make Fast Refresh fall back to a reload. Split exports and name the component {File trộn export component và không-component, hoặc dùng default export ẩn danh — cả hai làm Fast Refresh rơi về reload. Tách export và đặt tên component}.
Stretch {Nâng cao}: in the lab, increment the counter, edit the CSS (state kept), edit the leaf component (state kept), then edit utils.ts (state lost) — and map each to the propagation rules above {trong lab, tăng counter, sửa CSS (giữ state), sửa component lá (giữ state), rồi sửa utils.ts (mất state) — và ánh xạ từng cái với luật lan truyền ở trên}.
Key takeaways {Điểm chính}
- HMR replaces one module; live reload refreshes everything {HMR thay một module; live reload refresh tất cả}.
- A module that calls
import.meta.hot.accept()is an HMR boundary {Module gọiimport.meta.hot.accept()là một ranh giới HMR}. - Updates propagate up until a boundary accepts; no boundary → full reload {Cập nhật lan lên tới khi một boundary chấp nhận; không boundary → reload đầy đủ}.
- Framework plugins (React Fast Refresh, Vue) handle HMR automatically {Plugin framework (React Fast Refresh, Vue) lo HMR tự động}.
- One named component per file makes Fast Refresh reliable {Một component có tên mỗi file làm Fast Refresh đáng tin}.
Next up {Tiếp theo}
Part 6 — CSS handling: CSS Modules, PostCSS, preprocessors (Sass/Less), ?inline, code-split CSS, and how Vite handles styles in dev vs build {Phần 6 — Xử lý CSS: CSS Modules, PostCSS, preprocessor (Sass/Less), ?inline, CSS tách code, và cách Vite xử lý style khi dev vs build}.