Vue3 provide/inject状態管理時のナビゲーションガード制御Tips
何の記事か
- Vue3でprovide/injectを用いた状態管理を行なっている場合、vue-routerの
router.beforeEach
フック内で状態管理している状態を参照すると、undefined
が返ってきてしまう - そのため、ナビゲーションガードを制御する場合、
router.beforeEach
以外で制御するように工夫が必要
前提とする環境
例えば下記のようなvue環境があるとします。
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');
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
から取得するのではなく、引数で受け取るように変更します。
<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>
// <中略>
import type { MyUser } from './state';
// navigation guard 関数
export const navigationGuard = async (
to: RouteLocationNormalized,
from: RouteLocationNormalized,
myUser?: MyUser
) => {
if (myUser) {
// <中略>
} else return '/login'
}
// <中略>
注意点
二つほど注意点があります。
-
navigationGuard
で遷移をチェックするタイミングが遷移前から遷移後に変わっている - リダイレクト時、経由するルートのレンダリングが実行されてしまう
- 例えば、未ログインで
/
にアクセスしようとして/login
にリダイレクトされた場合、上記処理では一瞬/
のレンダリングが始まって表示されてしまう - 加えて、createdやonMountedなどに定義した取得処理なども走ってしまい余計なエラーを生み出してしまう
- 例えば、未ログインで
1. 遷移をチェックするタイミングが遷移前から遷移後に変わっている
これで特段navigationGuard
の処理が変わるということではないんですが、注意が必要です。
2. リダイレクト時、経由するルートのレンダリングが実行されてしまう
下記のようにルートが変更した際にredirectPath
がfalcy、つまりもうこれ以上リダイレクトする必要がないときにreadyForDisplay
をtrue
にすることで、それまでレンダリングが走らないようにすることができます。
<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でナビゲーションガードを実装する際の参考になれば幸いです。
Discussion