できない.dev

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 ログを確認するのが切り分けの起点。

よくある原因

  1. ファイル監視の不発: Docker / WSL2 / ネットワークマウント上では fs.watch がホスト側の変更を検知できない
  2. WebSocket 接続失敗: HTTPS リバースプロキシ越し、ポート転送越し、host 指定の不一致で client 接続が確立しない
  3. Fast Refresh の更新境界外: React コンポーネントが named export のみ、または同一ファイルから非コンポーネントも export している
  4. 副作用モジュール: グローバル 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());
}

この記事は役立ちましたか?