Storefront HMR シングルトンパターン¶
1. 背景¶
apps/storefront は Vite + React Router SPA であり、開発中は Vite の HMR(Hot Module Replacement)が使われる。
HMR でモジュールが再評価されると、モジュールスコープの変数(let/const)は初期値へ戻る。useRef もコンポーネントが再 mount されれば再初期化される。
この問題が顕在化したのは TanStack Query の QueryClient であった。StorefrontQueryProvider が useRef で QueryClient を保持していたため、HMR 後にキャッシュが消えて認証状態 (["activeCustomerContext"]) が anonymous に倒れ、お気に入りなど認証依存クエリが silent fail した。
2. 採用パターン: import.meta.hot.data¶
Vite 公式の HMR persistence API。HMR モジュール置換をまたいで任意の値を保持できる。本番ビルドでは import.meta.hot が undefined になるため、ガード内のコードは dead-code として除去される。
type StorefrontQueryClient = ReturnType<typeof createStorefrontQueryClient>;
function getOrCreateQueryClient(enableDebugLogging: boolean): StorefrontQueryClient {
if (import.meta.hot) {
const cached = import.meta.hot.data.queryClient as StorefrontQueryClient | undefined;
if (cached) return cached;
const client = createStorefrontQueryClient({ enableDebugLogging });
import.meta.hot.data.queryClient = client;
return client;
}
return createStorefrontQueryClient({ enableDebugLogging });
}
export function StorefrontQueryProvider({ children }) {
const { enableDebugLogging } = useStorefrontConfig();
const [queryClient] = useState(() => getOrCreateQueryClient(enableDebugLogging));
// ...
}
実装: apps/storefront/src/components/providers/storefront-query-provider.tsx
パターンの構成要素¶
| 要素 | 理由 |
|---|---|
import.meta.hot.data |
Vite 公式 HMR persistence。モジュール再評価をまたいで値が生存 |
useState(() => ...) |
TanStack Query 公式 SSR パターン。コンポーネント mount 中の安定性を保証 |
| 本番フォールバック | import.meta.hot が undefined の場合は通常の生成パスのみを通る |
3. 適用判断基準¶
HMR 後にユーザー可視の失敗(データ取得停止、認証状態リセット)を引き起こすシングルトンが対象。以下の基準で判断する。
| 状況 | 対応 |
|---|---|
| 失うと機能が停止する状態(認証キャッシュ、アクティブセッションなど) | import.meta.hot.data で保護 |
| 失っても再構築できるキャッシュ(TTL 付きパフォーマンスキャッシュなど) | 保護不要 |
useEffect 内の addEventListener |
React が cleanup を管理するため安全 |
window フラグ付きの一度だけ実行されるパッチ |
フラグが window に残るため安全 |
4. 既存コードの監査結果(2026-05 時点)¶
HMR 後のユーザー可視失敗を引き起こす箇所を全件調査した結果。
対応済み¶
| ファイル | 内容 | 対応 |
|---|---|---|
src/components/providers/storefront-query-provider.tsx |
QueryClient を useRef で保持 |
import.meta.hot.data + useState へ変更 |
保護不要と判定¶
| ファイル | 内容 | 理由 |
|---|---|---|
src/lib/visible-products.ts:76 |
visibleSearchResultCache (Map) |
TTL 15s のパフォーマンスキャッシュ。HMR で消えても次アクセスで再構築される |
src/lib/analytics/ga4.ts:53 |
window.dataLayer ??= [] |
関数内の初期化。??= で既存配列を保持するため HMR 安全 |
src/components/providers/storefront-replay-controller.tsx:29,53 |
history patch / addEventListener | window[HISTORY_PATCHED_FLAG] ガード済み / cleanup 関数つき外部ストア購読 |
src/mocks/browser.ts:22 |
window.__mswWorkerStarted |
開発用モック専用。機能失敗にならない |
5. 新規シングルトンを追加する際のルール¶
モジュールスコープに状態を持つオブジェクト(クライアント、Map、Promise)を追加する場合:
- HMR で失われた際にユーザー可視の失敗が起きるか確認する
- 起きる場合は
import.meta.hot.dataパターンを採用する import.meta.hot.dataのキーはモジュール間で衝突しないよう__ritsubi<ModuleName><ObjectName>__形式にする- TypeScript 型は
as Type | undefinedでキャストする(import.meta.hot.dataはany型のため)
6. 参考¶
- Vite 公式 HMR API: https://vite.dev/guide/api-hmr
- TanStack Query SSR セットアップ: https://tanstack.com/query/latest/docs/framework/react/guides/ssr
- 実装:
apps/storefront/src/components/providers/storefront-query-provider.tsx