Vite で HMR (Hot Module Replacement) が反映されない
Vite の HMR は ESM 経由のモジュール置換で動く。
ファイル監視・WebSocket 接続・Fast Refresh の更新境界・副作用モジュールのいずれかでつまずくと反映されない。
DevTools の Console と Network タブで状態を確認し、原因を順に切り分ける。
#vite#hmr#hot-reload#dev-server
公開:
要約
Vite の HMR が反映されない原因は、(1) ファイル監視が変更を拾えていない、(2) ブラウザと dev server の WebSocket が確立できていない、(3) React Fast Refresh の更新境界が認識されない、(4) 副作用モジュールを HMR で扱おうとしている、のいずれか。
まずブラウザ DevTools の Console と Network タブで client (WebSocket) の状態と HMR ログを確認するのが切り分けの起点。
よくある原因
- ファイル監視の不発: Docker / WSL2 / ネットワークマウント上では
fs.watchがホスト側の変更を検知できない - WebSocket 接続失敗: HTTPS リバースプロキシ越し、ポート転送越し、
host指定の不一致でclient接続が確立しない - Fast Refresh の更新境界外: React コンポーネントが named export のみ、または同一ファイルから非コンポーネントも export している
- 副作用モジュール: グローバル CSS 適用やシングルトンの初期化など、置換しても効果がないコード
解決策
1. ポーリングモードを有効にする
Docker / WSL 環境では fs.watch (inotify) が動かないため、server.watch options でポーリングへ切り替える。
// vite.config.ts
import { defineConfig } from "vite";
export default defineConfig({
server: {
watch: { usePolling: true, interval: 300 },
},
});2. HMR の接続先を明示する
リバースプロキシや HTTPS 越しに dev server を見ている場合、ブラウザが繋ぎに行く先を server.hmr で固定する。
export default defineConfig({
server: {
hmr: { protocol: "wss", host: "dev.example.com", clientPort: 443 },
},
});3. Fast Refresh の規約に合わせる
React の Fast Refresh は「ファイルが関数コンポーネントだけを export する」前提で更新境界を切る。
定数やユーティリティ関数を同居 export していると full reload に落ちる。
// NG: コンポーネントと定数が同居
export const COLORS = ["red", "blue"];
export default function Button() { /* ... */ }
// OK: 定数は別ファイルへ移し、Button.tsx は default export のみ4. 副作用モジュールは HMR API を使う
グローバル状態を触るコードでは HMR API を直接呼び、再評価後の処理を自前で書く。
import { applyTheme } from "./theme";
applyTheme();
if (import.meta.hot) {
import.meta.hot.accept("./theme", (m) => m?.applyTheme());
}