Vue3 provide/inject状態管理時のナビゲーションガード制御Tips

# 何の記事か

- Vue3でprovide/injectを用いた状態管理を行なっている場合、vue-routerの`router.beforeEach`フック内で状態管理している状態を参照すると、`undefined`が返ってきてしまう
- そのため、ナビゲーションガードを制御する場合、`router.beforeEach`以外で制御するように工夫が必要

# 前提とする環境

例えば下記のようなvue環境があるとします。

```typescript:main.ts
import { createApp } from 'vue';
import router from './router';
import { keyMyUser, createDefaultMyUser } from './state';
const app = createApp();
app.use(router);
app.provide(keyMyUser, createDefaultMyUser());
app.mount('#app');
```

```typescript:router.ts
import { createRouter, createWebHistory, Router, RouteLocationNormalized } from 'vue-router';
import { useMyUser } from './state';
import { routes } from './routes';

// navigation guard 関数
export const navigationGuard = async (
  to: RouteLocationNormalized,
  from: RouteLocationNormalized,
) => {
  const myUser = useMyUser() // ← ここが問題 undefinedを返してしまう
  if (myUser) {
    // <中略>
  } else return '/login'
}

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes,
});
router.beforeEach(navigationGuard);
export default router;
```

この`router.ts`内の`router.beforeEach(navigationGuard)`で設定した`navigationGuard`関数内で参照した`useMyUser()`が`undefined`を返してしまい、正しくルーティングの制御をするのが難しい

# 対応策

まず`useMyUser()`が`undefined`を返さず正しく値を返すようにするために`script setup`または`setup()`の中で`navigationGuard`を参照するようにする必要があります。

> 繰り返しますが、もし \<script setup\> を使用しないのであれば、inject() は setup() の内部でのみ同期的に呼び出す必要があります:
> https://ja.vuejs.org/guide/components/provide-inject.html#inject

`app.vue`で`router.beforeEach`の代わりに`watch(router.currentRoute,() => {})`を使ってrouteの変更を検知します。

加えて、`myUser`を`router.ts`内でimportした`useMyUser`から取得するのではなく、引数で受け取るように変更します。

```vue:app.vue
<template>
  <router-view />
</template>
<script setup lang="ts">
import { watch } from 'vue';
import { useRouter } from 'vue-router';
import { navigationGuard } from './router';
const router = useRouter();
watch(router.currentRoute, async (newValue, oldValue) => {
  const redirectPath = await navigationGuard(
    newValue.path,
    oldValue.path,
  );
  if (redirectPath) router.push(redirectPath);
});
</script>
```

```typescript:router.ts
// <中略>
import type { MyUser } from './state';

// navigation guard 関数
export const navigationGuard = async (
  to: RouteLocationNormalized,
  from: RouteLocationNormalized,
  myUser?: MyUser
) => {
  if (myUser) {
    // <中略>
  } else return '/login'
}
// <中略>
```

# 注意点

二つほど注意点があります。

1. `navigationGuard`で遷移をチェックするタイミングが**遷移前**から**遷移後**に変わっている
2. リダイレクト時、経由するルートのレンダリングが実行されてしまう
   - 例えば、未ログインで`/`にアクセスしようとして`/login`にリダイレクトされた場合、上記処理では一瞬`/`のレンダリングが始まって表示されてしまう
   - 加えて、createdやonMountedなどに定義した取得処理なども走ってしまい余計なエラーを生み出してしまう

## 1. 遷移をチェックするタイミングが**遷移前**から**遷移後**に変わっている

これで特段`navigationGuard`の処理が変わるということではないんですが、注意が必要です。

## 2. リダイレクト時、経由するルートのレンダリングが実行されてしまう

下記のようにルートが変更した際に`redirectPath`がfalcy、つまりもうこれ以上リダイレクトする必要がないときに`readyForDisplay`を`true`にすることで、それまでレンダリングが走らないようにすることができます。

```vue:app.vue
<template>
  <router-view v-if="readyForDisplay"/>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { navigationGuard } from './router';
import { useMyUser } from './state';
const router = useRouter();
const readyForDisplay = ref<boolean>(false);
const myUser = useMyUser()
watch(router.currentRoute, async (newValue, oldValue) => {
  const redirectPath = await navigationGuard(
    newValue.path,
    oldValue.path,
    myUser.value,
  );
  if (redirectPath) router.push(redirectPath);
  else readyForDisplay.value = true
});
</script>
```

これからvueでナビゲーションガードを実装する際の参考になれば幸いです。