bitA Tech Blog
🪤

Vue Router のonBeforeRouteUpdate で値が更新されない?注意したいポイントとデバッグ時の罠

に公開

🎄 本記事は、bitA Tech Blog Advent Calendar 5日目の記事です。

私は、Vue.jsを利用したSPAの開発や保守に携わることが多く、その中で得た知見を紹介します。

この記事で話すこと

本記事では、ルートナビゲーション処理をフックするためにVue Router が用意しているナビゲーションガード関数onBeforeRouteUpdate を使う際に、開発で遭遇した問題とその回避方法を紹介します。
特に「つまずきやすいポイント(落とし穴)」にも触れ、同じミスを避けるための注意点を解説するのでぜひ参考にしてみてください。

onBeforeRouteUpdate とは

onBeforeRouteUpdate は、同じコンポーネント内でルートパラメーターが変更される直前に実行される関数です。この『ルート変更前』に必要な処理を任意で差し込むことができます。

https://router.vuejs.org/api/functions/onBeforeRouteUpdate.html

遭遇した問題

今回遭遇した問題は、ルートパラメーターの更新に応じて、ドロワーの内容を更新する処理が正しく動作しなかったことです。以下で、挙動を再現する簡単な例をもとに説明します。

具体例

下記の画像のように、サービス一覧とその詳細を表示するUIとします。ユーザーが一覧から目的のサービスをクリックすると、右側からサービス詳細ドロワーがスライド表示される想定です。

操作と期待する挙動

【前提】

  • 一覧ページに、「service 1」の詳細ドロワーが表示されている状態(パス: /service/1)。

【操作】

  • サービスの一覧から「service 2」をクリックする。

【挙動】

  • ルートパラメーター(id)を使って、次に表示するデータを取得する。
  • 取得したデータを使って、詳細ドロワーに「service 2」の内容を表示する(パス: /service/2)。

実装

この挙動を実現するために、「ルートパラメーターの更新前にドロワーのデータを更新する」処理を入れています。以下のサンプルコードでは、記事の主旨と関係のない部分(データ取得やスタイルなど)は簡略化しています。

サンプルコード

Service.vue
<script setup lang='ts'>
import { useRoute } from 'vue-router';

const route = useRoute();

// template 内で表示するデータ
const detailData = ref();

// 表示するデータを取得し、リアクティブ変数'detailData'に渡す
const fetchData = async (id: string) => {
  // 処理(省略)
}

onBeforeRouteUpdate(async () => {
  // ルートパラメーターに含まれるidを取得する
  const id = route.params.id as string;
  await fetchData(id);
})
</script>

<template>
  <h2>Service Page</h2>
  <nav>
    <button type="button" @click="handleDetailPage(1)">
      Service 1
    </button>
    <button type="button" @click="handleDetailPage(2)">
      Service 2
    </button>
  </nav>
  <div v-if="detailData">
    <ServiceDetail :data="detailData" />
  </div>
</template>

<style>
/* 省略 */
</style>

ディレクトリ構成

src/
├─ components/
│   └─ ServiceDetail.vue      # サービス詳細ドロワー
├─ pages/
│   └─ Service.vue            # サービス一覧
├─ router/
│   └─ index.ts               # /service/:id のルート定義
├─ App.vue                  # デモのため、template内に<router-view />を配置
└─ main.ts

実行環境

  • Vue v3.5.25
  • Vue Router v4.6.3
  • TypeScript v5.9.3

一見すると、正しく動作しそうですが、実際には注意すべき2つの落とし穴がありました。

落とし穴①:ナビゲーションガード内でrouteを参照する危険性

1つ目の問題は、onBeforeRouteUpdate 内でリアクティブな値(route)を参照し、データ取得していた点にあります。

Service.vue
import { useRoute } from 'vue-router';
const route = useRoute();
// 省略
onBeforeRouteUpdate(async () => {
  // ルートパラメーターに含まれるidを取得する
  const id = route.params.id as string;
  await fetchData(id);
})

route は、Vue Routerの useRoute() が返すリアクティブオブジェクトですが、onBeforeRouteUpdate が実行されるタイミングでは、ナビゲーションがまだ確定していないため、route の値は次のルートの内容に更新されていません。

そのため、操作と期待する挙動で示した「service 2を選択する」操作を行うと、

  • パスは /service/2 へ更新される
  • ドロワーの内容は「service 1」から更新されない

という不具合が発生します。

これは、route.params.id がまだ古い値(1)のままで、その値を使ってデータの取得処理を行ったためです。

このような事象を防ぐため、onBeforeRouteUpdate では、3つの引数(to, from, next を持つ)が利用できます。このうち、toこれから遷移する先のルート情報を持っているため、今回の用途に最適です。

修正例は次のとおりです。

Service.vue
- onBeforeRouteUpdate(async () => {
+ onBeforeRouteUpdate(async (to) => {
    // ルートパラメーターに含まれるidを取得する
-   const id = route.params.id as string;
+   const id = to.params.id as string;
    await fetchData(id);
  })

実装したい用途によりますが、この修正で1つ目の落とし穴は確実に回避できます。

落とし穴②:デバッグ時に起きる『錯覚』

2つ目は、Chrome DevToolsを使ったデバッグ時に発生します。
routeをログ表示すると、一見、「遷移先の値が入っているように見えてしまう」点が厄介です。

まず、routeonBeforeRouteUpdate 内でログ表示します。

Service.vue
  onBeforeRouteUpdate(async (to) => {
+   console.log('route', route);
    // ルートパラメーターに含まれるidを取得する
    const id = to.params.id as string;
    await fetchData(id);
  })

VueのリアクティビティはProxyを使っており、DevTools上では、ログの結果がProxy オブジェクト として表示されます。そして、このProxyを展開すると、次のように遷移先のid が表示されます。

これは一見、正しい動作に見えますが、実はChrome DevToolsの挙動による錯覚です。

何が、錯覚なのか...?

これを確認するために、先ほどのコードにルートパラメーターへ直接ログする記述(route.params.id)を追加し、見比べてみます。

Service.vue
  onBeforeRouteUpdate(async (to) => {
+   console.log('route.params.id', route.params.id);
    console.log('route', route);
    // ルートパラメーターに含まれるidを取得する
    const id = to.params.id as string;
    await fetchData(id);
  })

結果、route.params.id は旧値の1route を展開し表示したid は新しい値である2 を表示しています。この差異が、錯覚の正体です。
これは、Vue Routerの仕様ではなく、Chrome DevToolsのプロパティを展開したタイミングでgetterを評価する仕様に由来します。

https://developer.chrome.com/docs/devtools/console/reference#evaluate-custom-accessors

この「見た目の罠」に引っかかると、「ナビゲーションガード内でroute を使っても問題ない」と錯覚を起こしたまま実装してしまい、期待と異なる結果を招くことがあり、注意が必要です。

この錯覚を回避し、その時点でのProxyオブジェクトの値を確認するには、ログ表示の工夫が必要です。以下の3つの方法があります。

  • 対象となる値を直接ログ表示する(例でもあったルートパラメーターを直接ログした方法)
  • Vue のtoRaw()関数を使い、Proxyのラップを除去して、生のオブジェクトを表示する
  • JSON経由でProxyのラップを除去して、生のオブジェクトを表示する

これらの方法とリアクティブなroute をログした結果を見比べてみます。

Service.vue
  onBeforeRouteUpdate(async (to) => {
+   console.log('route', route);
+   console.log('route.params.id', route.params.id);
+   console.log('toRaw', toRaw(route.params));
+   console.log('json - parse', JSON.parse(JSON.stringify(route)));
    // ルートパラメーターに含まれるidを取得する
    const id = to.params.id as string;
    await fetchData(id);
  })

プロパティを展開したroute では、id2 であるに対して、回避策の3つでは、id1 になっています。いずれの方法でも、その時点での値の表示ができていそうです。

以上のことから、

  • DevToolsの仕様を知っておくこと
  • ProxyオブジェクトのProxyラップを除去して、生のオブジェクトをログ表示すること

の2つが、『デバッグの罠』の回避策です。

まとめ

今回は onBeforeRouteUpdate を使う際に注意したい2つの落とし穴と、その回避策について紹介しました。

  1. ガード内ではroute ではなく引数のto を使う
    routeuseRoute())は、ナビゲーションが完了するまで更新されません。「遷移先の値が必要な処理」では、常にナビゲーションガードの引数であるto を利用する必要があります。
  2. DevTools の表示に惑わされないこと
    DevToolsはプロパティ展開時にgetterを評価するため、「ログ出力時点の値と表示」が一致しない場合があります。正しく挙動を確認するには、『この仕様を理解すること』・『ProxyオブジェクトのProxyラップを除去し、生のオブジェクトをログ表示すること』が有効です。

とくに、2つ目のDevToolsによる錯覚は気づきにくいポイントです。本記事が、同じ事象で悩む方の助けになれば嬉しいです!

bitA Tech Blog
bitA Tech Blog

Discussion