ViteのHMRをデバッグし、開発者体験を取り戻す方法
今日困って調べたので備忘録と共有がてら記事にしておきます
結論
「ルーク、 vite --debug hmr
を使え」
「 circular imports detected
を直すんだ、ルーク」
以下は解説です。
ViteのHMR(Hot Module Replacement)
Viteは開発者体験に着目したビルドツールであり、その機能の一つにHMRと呼ばれる仕組みがあります。
通常ではJavaScriptファイルに対して何らかの変更を加えた場合、ブラウザをリロードして変更を読み込む必要がありますが、これは開発イテレーションを回すのが遅くなりがちで開発者体験が良くありません。
この問題を解消する仕組みの一つがHMRです。
Viteサーバはブラウザに対してWebSocketで通信を行い、変更があったモジュールおよび、そのモジュールに依存するモジュールのみを動的に再読み込みします。
この仕組みをHMRと呼びます。
Viteが開発者体験を追及しているのは伊達ではなく、このHMRの仕組みは複雑な設定をすることなく create-vite
を介してアプリケーションを作成する場合この設定は構成済みになっています。
詳しくはViteのドキュメントを参考にしてください。
起こっていた問題
一か所でしか使われていないReactコンポーネントを保存しただけなのにページのトップレベルに近いコンポーネントのHMRが行われてしまうという問題が発生していました。
この問題が起こった時、HMRが行われたコンポーネントがProviderのような上位コンポーネントでアプリケーション全体、あるいは広範囲のコンポーネントだった場合、以下のような問題が起こり開発者体験がとても悪くなります。
- 下位コンポーネント全ての再レンダリングが行われる
- useEffectやuseStateなどで保持している状態がリセットされる
ちょっと見た目をいじりたいだけなのにアプリケーション全体が初期状態にリロードされてしまうのは開発イテレーションの低下を招きます。
また、このような問題が起きなかったとしても再読み込みされるコンポーネントが多いということはブラウザへの反映がそれだけ遅くなるということでもあり、アプリケーションが大きくなるほど問題になる可能性があります。
ViteのHMRをデバッグする方法
答えはトラブルシューティングのHMRのセクションに書かれています。
MR が処理されているものの、それが循環依存関係の中にある場合、実行順序を回復するためにフルリロードも起こります。これを解決するには、そのループを解除してみてください。vite --debug hmr を実行することで、ファイル変更がトリガーとなった場合に、循環依存関係のパスをログに残すことができます。
-
vite --debug hmr
を使ってサーバを起動してログを見ましょう。 -
circular imports detected
が出ているコンポーネントの依存関係を見直しましょう
※ vite -d
でフルデバッグログが表示されます
vite:hmr circular imports detectedの解消
問題となるコンポーネントがある場合、以下のようなログが表示されます。
vite:hmr circular imports detected: /src/components/providers/VoiceConnectionProvider/ConfirmConnectToWorkspace.tsx -> /src/components/layout/HomePageLayout.tsx -> /src/components/providers/index.ts -> /src/components/providers/VoiceConnectionProvider/index.ts -> /src/components/providers/VoiceConnectionProvider/VoiceConnectionProvider.tsx -> /src/components/providers/VoiceConnectionProvider/ConfirmConnectToWorkspace.tsx +0ms
今回保存したファイルは components/providers/VoiceConnectionProvider/ConfirmConnectToWorkspace.tsx
で、このファイルは以下のような循環参照になってしまっています。
- components/providers/VoiceConnectionProvider/ConfirmConnectToWorkspace.tsx ->
- components/layout/HomePageLayout.tsx ->
- components/providers/index.ts ->
- components/providers/VoiceConnectionProvider/index.ts ->
- components/providers/VoiceConnectionProvider/VoiceConnectionProvider.tsx ->
- components/providers/VoiceConnectionProvider/ConfirmConnectToWorkspace.tsx
もう少し掘り下げて見ましょう。
components/providers/VoiceConnectionProvider/ConfirmConnectToWorkspace.tsx
には以下のようなimport文が存在します。
import { HomePageLayout } from "@/components/layout/HomePageLayout"
次に、 components/layout/HomePageLayout.tsx
では以下のようなimport文が存在します。
import { useAuth } from "../providers"
ここで ../providers
、すなわち components/providers
には以下のようなexport文があります。
export * from "./VoiceConnectionProvider"
もちろんこの VoiceConnectionProvider
では以下のように最初のコンポーネントを読み込んでいます。
import { ConfirmConnectToWorkspace } from "./ConfirmConnectToWorkspace"
HMR対象のコンポーネントが循環参照しているモジュールだった場合、フルリロードが行われるケースがあります。
まとめ
viteのHMRがなんかおかしいな、と思ったらたまには --debug hmr
で様子を見てみましょう。
あるいは普段からこのオプションを有効にしておくと早く気づけるかもしれませんね。
そもそも普段からコンポーネントの循環参照には気を付けていきたいところであります。
Discussion