Reputation: 2560
I have a PhotoDetail component on a single-page application that is set up to accept dynamic route parameters, per Vue Router 4.x documentation, and reads data from a Pinia store. I have an onBeforeMount() lifecycle handler to check if the ID in the route param matches the same ID in my store (via loadOnePicture() Pinia action). If not, I want to send the user the my homepage component. For example, http://localhost:3000/photo/1 would be valid in my case, but http://localhost:3000/photo/asdfasdf would not be valid, and I should be sent back to the Homepage component and the Homepage component re-renders.
However, the code isn't working as intended. While the router does change over to the Home component (and I confirmed this with the Vue dev tool), the resulting page is blank in the router-view section. In the console, I'm seeing two Vue warnings and an error. The error is weird to me, because it references a data property (photo) that is only present on the PhotoDetail component. The router moved me to the Home component, but it seems Vue is still processing the PhotoDetail component.
Does anybody know what I am doing wrong?
Warnings and Errors:
[Vue warn]: Unhandled error during execution of render function at <PhotoDetail> at <Photo onVnodeUnmounted=fn<onVnodeUnmounted> ref=Ref< undefined > > at <RouterView> at <App> [runtime-core.esm-bundler.js:38:16](http://localhost:3000/node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js)
[Vue warn]: Unhandled error during execution of scheduler flush. This is likely a Vue internals bug. Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/core at <PhotoDetail> at <Photo onVnodeUnmounted=fn<onVnodeUnmounted> ref=Ref< undefined > > at <RouterView> at <App>
Uncaught (in promise) TypeError: $setup.photo.imageUrls is undefined
_sfc_render PhotoDetail.vue:64
Code:
<script setup>
import { onBeforeMount, watch } from "vue";
import { ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useStore } from '@/stores/photoStore.js';
const store = useStore();
const route = useRoute();
const router = useRouter();
const photo = ref({});
onBeforeMount(
() => {
let photoObj = '';
photoObj = store.loadOnePicture(parseInt(route.params.id, 10));
/* If we cannot find an image, send user back to homepage */
if (typeof photoObj === 'undefined') {
router.replace({ name: 'home'});
} else {
photo.value = photoObj;
}
}
);
function getSizes(photo) {
return '(max-width: 40rem) 300px, 600px';
}
function getSrcs(photo) {
return photo.imageUrls.detail + ' 300w,' + photo.imageUrls.detail2x + ' 600w';
}
</script>
<template>
<div class="photo-card-wrap">
<div class="photo-card">
<div class="photo-card-info prose">
<h5 class="photo-title" v-if="photo.title">{{ photo.title }}</h5>
<p class="photo-text" v-if="photo.character"><strong>Character:</strong> {{ photo.character }}</p>
<p class="photo-text" v-if="photo.cosplayer"><strong>Cosplayer(s):</strong> {{ photo.cosplayer }}</p>
<p class="photo-text" v-if="photo.source"><strong>Series:</strong> {{ photo.source }}</p>
<p class="photo-text" v-if="photo.convention"><strong>Convention:</strong> {{ photo.convention }}</p>
<p class="photo-text" v-if="photo.notes">{{ photo.notes }}</p>
</div>
<div class="photo-card-img">
<img class="photo-details-img lazyload"
:data-src="photo.imageUrls.detail"
:data-sizes="getSizes(photo)"
:data-srcset="getSrcs(photo)"
:alt="photo.title" :title="photo.title" />
</div>
</div>
</div>
</template>
Upvotes: 1
Views: 2190
Reputation: 2560
The solution I came up with is to use a per-route navigation guard as described here. https://router.vuejs.org/guide/advanced/navigation-guards.html#per-route-guard
As Estus Flask's comment pointed out to me, I cannot interrupt the component lifecycle.
I don't know if my solution is the best solution, but it worked for my use case.
My routes were stored in router/index.js. In my main.js where Vue and Pinia are instantiated, I had to import router/index.js after I started up Pinia, so I could call useStore() within router/index.js.
// main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import './index.css';
import App from './App.vue';
const app = createApp(App);
app.use(createPinia());
/* Router has a beforeEnter guard dependent on Pinia being instantiated first, which is why this import statement is here. */
import router from './router/index.js';
app.use(router);
app.mount('#app');
When I define the routes, I can now use beforeEnter() to check the store to see if the data is there. If the data is there, do nothing. If the data is not there, return a route object with the name of the 'notfound' component.
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Home from '../views/Home.vue';
import { useStore } from '@/stores/photoStore.js';
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
beforeEnter: (to, from) => {
const store = useStore();
/* If there are no photos, send user to Not Found page */
if (typeof store === 'undefined') {
return {
name: 'notfound'
}
}
},
component: Home,
name: 'home'
},
{
path: '/photo/:id',
beforeEnter: (to, from) => {
let photoObj = '';
const store = useStore();
photoObj = store.loadOnePicture(parseInt(to.params.id, 10));
/* If we cannot find an image, send user to Not Found page */
if (typeof photoObj === 'undefined') {
return {
name: 'notfound'
}
}
},
component: () => import('../views/Photo.vue'),
name: 'photo'
},
{
path: '/about',
component: () => import('../views/About.vue'),
// component: About,
name: 'about'
},
{
path: '/contact',
// component: Contact,
component: () => import('../views/Contact.vue'),
name: 'contact'
},
{
/* Wildcard path to catch other paths */
path: '/:pathMatch(.*)*',
name: 'notfound',
component: () => import('../views/NotFound.vue')
}
]
})
export default router;
Upvotes: 2