📚

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

2023/11/30に公開

何の記事か

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

前提とする環境

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

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');
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.vuerouter.beforeEachの代わりにwatch(router.currentRoute,() => {})を使ってrouteの変更を検知します。

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

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>
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、つまりもうこれ以上リダイレクトする必要がないときにreadyForDisplaytrueにすることで、それまでレンダリングが走らないようにすることができます。

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でナビゲーションガードを実装する際の参考になれば幸いです。

GitHubで編集を提案

Discussion