• #vue
  • #nuxt
  • #reactivity
  • #bug-fix
  • #cloudflare
未分類

コーディング規約ビューアのTOC開閉バグ修正

問題の症状

発生環境

  • ローカル開発: 問題なし
  • Cloudflare Pages デプロイ後: Table of Contents(目次)のセクション開閉ができない

具体的な症状

  • 左サイドバーのTable of Contentsで、セクションタイトルをクリックしても開閉されない
  • セクションの開閉状態が変更されない
  • スクロールによる自動開閉も動作しない

原因

VueのリアクティビティとSetの問題

VueのリアクティブシステムはSetオブジェクトの変更を適切に検知できない場合があります。

問題のコード:

const openSections = ref(new Set());

function toggleSection(sectionTitle) {
  if (openSections.value.has(sectionTitle)) {
    openSections.value.delete(sectionTitle);  // ❌ リアクティブに検知されない
  } else {
    openSections.value.add(sectionTitle);     // ❌ リアクティブに検知されない
  }
}

watch(activeIndex, (newIndex) => {
  const currentStep = steps.value[newIndex];
  if (currentStep && currentStep.section) {
    openSections.value.clear();               // ❌ リアクティブに検知されない
    openSections.value.add(currentStep.section);
  }
});

なぜローカルでは動作したのか?

  • 開発環境とプロダクション環境でのVueのビルド設定の違い
  • Hot Module Replacement (HMR)による強制的な再レンダリング
  • デバッグモードでの追加のリアクティビティチェック

なぜデプロイ後に問題が発生したのか?

  1. プロダクションビルドの最適化: Viteのプロダクションビルドでは、リアクティビティの検知が厳密になる
  2. キャッシュの影響: ユーザーが指摘した通り、古いバージョンのキャッシュを見ていた可能性もある
  3. Cloudflare環境の違い: エッジでの実行環境がローカルと異なる可能性

解決方法

SetからArrayへの変更

Setの代わりに配列を使用することで、Vueのリアクティブシステムが確実に変更を検知できるようにしました。

修正後のコード:

// Setから配列に変更
const openSections = ref([]);

function toggleSection(sectionTitle) {
  const index = openSections.value.indexOf(sectionTitle);
  if (index > -1) {
    openSections.value.splice(index, 1);  // ✅ リアクティブに検知される
  } else {
    openSections.value.push(sectionTitle);     // ✅ リアクティブに検知される
  }
}

function isSectionOpen(sectionTitle) {
  return openSections.value.includes(sectionTitle);
}

watch(activeIndex, (newIndex) => {
  const currentStep = steps.value[newIndex];
  if (currentStep && currentStep.section) {
    // 配列の再代入で確実にリアクティブ
    openSections.value = [currentStep.section];  // ✅ リアクティブに検知される
  }
});

変更内容の詳細

項目変更前(Set)変更後(Array)
初期化ref(new Set())ref([])
追加.add(item).push(item)
削除.delete(item).splice(index, 1)
存在チェック.has(item).includes(item)
クリア.clear()value = []

修正ファイル

  • apps/web/app/pages/coding-standards/viewer/index.vue

テスト結果

ローカル環境

  • ✅ セクションの開閉が正常に動作
  • ✅ スクロールによる自動開閉が正常に動作
  • ✅ 前へ/次へボタンによる移動時の自動開閉が正常に動作

デプロイ環境(Cloudflare Pages)

  • ✅ セクションの開閉が正常に動作
  • ✅ スクロールによる自動開閉が正常に動作
  • ✅ 前へ/次へボタンによる移動時の自動開閉が正常に動作

教訓

Vueのリアクティビティのベストプラクティス

  1. 配列を使う: 可能な限り配列を使用する
  2. refの値を直接変更: ref.value = newValue の形で代入する
  3. Setを使う場合: 必ず値全体を再代入する
    // ❌ 悪い例
    mySet.value.add(item)
    
    // ✅ 良い例
    mySet.value = new Set([...mySet.value, item])
    

デプロイ前のチェックリスト

  1. ✅ ローカルでのテスト
  2. ✅ プロダクションビルドでのテスト(pnpm build && pnpm preview
  3. ✅ リアクティビティが必要な状態管理は配列または単純なオブジェクトを使用
  4. ✅ デプロイ後のキャッシュクリアを確認

関連リンク

まとめ

VueのリアクティブシステムはSetの変更を適切に検知できない場合があるため、開閉状態の管理には配列を使用することが推奨される。ローカル環境で問題なく動作していても、プロダクション環境で問題が発生する可能性があるため、プロダクションビルドでのテストが重要である。