jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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.

6 MIN READ

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}
datapersist 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}:

  1. If the module self-accepts, swap it in place. Done {Nếu module tự chấp nhận, thay tại chỗ. Xong}.
  2. 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}.
  3. 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-react enables 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-vue does 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ọi import.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}.