🐛

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のドキュメントを参考にしてください。
https://ja.vite.dev/guide/features.html#hot-module-replacement

起こっていた問題

一か所でしか使われていないReactコンポーネントを保存しただけなのにページのトップレベルに近いコンポーネントのHMRが行われてしまうという問題が発生していました。

この問題が起こった時、HMRが行われたコンポーネントがProviderのような上位コンポーネントでアプリケーション全体、あるいは広範囲のコンポーネントだった場合、以下のような問題が起こり開発者体験がとても悪くなります。

  • 下位コンポーネント全ての再レンダリングが行われる
  • useEffectやuseStateなどで保持している状態がリセットされる

ちょっと見た目をいじりたいだけなのにアプリケーション全体が初期状態にリロードされてしまうのは開発イテレーションの低下を招きます。
また、このような問題が起きなかったとしても再読み込みされるコンポーネントが多いということはブラウザへの反映がそれだけ遅くなるということでもあり、アプリケーションが大きくなるほど問題になる可能性があります。

ViteのHMRをデバッグする方法

答えはトラブルシューティングのHMRのセクションに書かれています。
https://ja.vite.dev/guide/troubleshooting.html#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 で様子を見てみましょう。
あるいは普段からこのオプションを有効にしておくと早く気づけるかもしれませんね。

そもそも普段からコンポーネントの循環参照には気を付けていきたいところであります。

PortalKey Tech Blog

Discussion