• #tax-assistant
  • #Vue.js
  • #Nuxt 3
  • #composable
  • #Vue Router
  • #リファクタリング
開発tax-assistantメモ

Vue.js タブURL同期のリファクタリングとブラウザバック問題の修正

tax-assistantの各タブでURLクエリパラメータの管理が重複していたため、useTabQuerySync composableに共通化した。また、ブラウザの戻る操作(Alt+矢印キー)が効かない問題も修正した。

背景

問題点

  1. コードの重複: 各タブ(ReceiptTab, ResultTab, DuplicateView等)で同じようなURL更新処理が書かれていた
  2. Alt+矢印キーが効かない: ShiwakeRulesViewのキーボードハンドラがAlt修飾キーをチェックせず、ブラウザバックをブロックしていた
  3. 履歴管理の混乱: router.pushrouter.replaceの使い分けが不適切で、戻るボタンで意図したページに戻れない
  4. タブ切り替え時の二重更新: タブ変更時にURL更新が複数回発火し、履歴が上書きされる

解決策

useTabQuerySync composable

タブのURLクエリパラメータ管理を共通化するcomposableを作成。

// composables/useTabQuerySync.ts
export function useTabQuerySync(options: {
  tabName: string
  buildQuery: () => Record<string, string | undefined>
  restoreFromQuery: (query: LocationQuery) => void
}) {
  const route = useRoute()
  const router = useRouter()
  const isInitialized = ref(false)
  const isActive = inject<Ref<string>>('currentTab')

  // URLクエリパラメータを更新
  function updateQueryParams(push = false) {
    if (!isActive?.value || isActive.value !== options.tabName) return
    if (!isInitialized.value) return

    const query = { tab: options.tabName, ...options.buildQuery() }
    // undefined値を除外
    const cleanQuery = Object.fromEntries(
      Object.entries(query).filter(([_, v]) => v !== undefined)
    )

    if (push) {
      router.push({ query: cleanQuery })
    } else {
      router.replace({ query: cleanQuery })
    }
  }

  // タブがアクティブになった時にURLを更新
  watch(
    () => isActive?.value,
    (newTab, oldTab) => {
      if (newTab === options.tabName && oldTab !== options.tabName) {
        nextTick(() => {
          updateQueryParams(true) // タブ切り替えはpush
        })
      }
    }
  )

  // 初期化完了をマーク
  function markInitialized() {
    isInitialized.value = true
  }

  return { updateQueryParams, markInitialized, isInitialized }
}

履歴ポリシーの使い分け

操作方法理由
タブ切り替えrouter.push戻るボタンで前タブに戻れるように
フィルタ変更(年月等)router.replace細かい履歴を積まない
帳票クリック遷移router.push元の画面に戻れるように

Alt+矢印キーの修正

キーボードイベントハンドラでAlt修飾キーをチェックし、ブラウザナビゲーションをブロックしないように修正。

// Before
function handleKeydown(e) {
  if (e.key === 'ArrowLeft') {
    e.preventDefault() // ← ブラウザバックもブロックしていた
    goToPrev()
  }
}

// After
function handleKeydown(e) {
  if (e.altKey) return // Alt+矢印はブラウザナビゲーション用
  if (e.key === 'ArrowLeft') {
    e.preventDefault()
    goToPrev()
  }
}

遷移時の履歴上書き防止

クレカ明細から帳票に遷移する際、複数のrouter.replaceが発火してrouter.pushの効果が消えていた問題を修正。

// index.vue - handleGoToReceipt
const isNavigatingToReceipt = ref(false)

function handleGoToReceipt(data: { batchId: string, fileName: string }) {
  // フラグを立てて、watcherでのreplace更新を抑制
  isNavigatingToReceipt.value = true

  // まずpushで履歴を追加
  router.push({
    query: {
      tab: 'receipt',
      batch: data.batchId,
      receiptFile: data.fileName,
      // ... 他のパラメータ
    }
  })

  // タブ切り替え
  currentTab.value = 'receipt'

  // 次のtickでフラグをクリア
  nextTick(() => {
    isNavigatingToReceipt.value = false
  })
}

// ReceiptTab.vue - watcherでフラグを確認
const isNavigatingToReceipt = inject<Ref<boolean>>('isNavigatingToReceipt')

watch(currentItem, () => {
  if (isNavigatingToReceipt?.value) return // 遷移中はスキップ
  updateQueryParams(false)
})

修正したファイル

ファイル変更内容
useTabQuerySync.ts新規作成
ShiwakeRulesView.vuecomposable適用、Alt+矢印対応
CreditCardMatchingView.vuecomposable適用
SquareMatchingView.vuecomposable適用
ReceiptTab.vuecomposable適用、遷移フラグ対応
ResultTab.vuecomposable適用
DuplicateView.vuecomposable適用
MillerColumnsView.vuecomposable適用、Alt+矢印対応
MatrixView.vuecomposable適用(新規追加)
index.vueisNavigatingToReceiptフラグ追加

テスト結果

  1. Alt+矢印キー: 全タブでブラウザバックが正常に動作
  2. タブ切り替え: 月次推移表 → 重複チェック → 戻るで正しく戻れる
  3. 帳票遷移: クレカ明細 → 帳票 → 戻るでクレカ明細に戻れる
  4. 連続クリック: 1ボタン → 戻る → 2ボタン → 戻るで毎回正しく戻れる

学び

  • Vue Routerのpushreplaceの違いを意識する: 履歴に残すかどうかで使い分け
  • イベントハンドラでブラウザのデフォルト動作をブロックしないよう注意: Alt, Ctrl, Meta修飾キーをチェック
  • composableで状態管理を共通化する際は、競合条件に注意: フラグやnextTickで制御