Why is a route not reactive? - vue.js

As part of my Quasar app, I have the following route:
import { RouteRecordRaw} from 'vue-router'
import { uid } from 'quasar'
const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: () => {
console.log('matched /')
return {path: `/${uid()}`}
}
},
{
path: '/:uuid',
component: () => import('pages/User.vue')
// component: User,
},
];
export default routes;
This works fine when going to /: the URL is changed to /73a219e5-2cf2-4dd0-8... and User.vue is executed (specifically there a fetch inside that retrieves some data based on the :uuid parameter.
If I force a route from within a component (User.vue for instance), via
import { useRouter } from 'vue-router'
const router = useRouter()
router.push('/')
I do see that the URL changes to a new UUID but User.vue is not executed. Specifically, a reference to route.params.uuid where const route = useRoute() is not reactive.
Is this normal (= I have to look for anther way to trigger), or is there a misuse (erroneous use) on my side?

The core of the issue is that you're (re)using the same component for rendering the page you're navigating from and the page you're navigating to.
By design, Vue optimises DOM rendering and will reuse the existing component instance. This means certain hooks won't be triggered (e.g: mounted, created, etc...) when changing route.
To force Vue into creating a different component instance when the route changes, use the current route's .fullPath as key on <router-view>:
<template>
...
<router-view :key="route.fullPath"></router-view>
...
</template>
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute();
</script>

Related

Vue route with dynamic import causes default layout to flash for split of second

I'm using Vue 3 with vue-router and vite-plugin-pages, which by default loads all route components asynchronously.
I have made a simple layout component which switches between LayoutDefault and LayoutBlank, based on the route's meta. Blank is only for public sites like the login page, where user is not yet logged in. I made it this way so I only need to set the layout on few routes instead of all of them.
Because of the dynamic import though, when entering the site and being redirected to the login site via router's beforeEach guard, you can see the default layout being drawn for a split of a second and then switches to blank layout. This happens, because route.meta.layout in the watcher is always undefined at the beginning.
I could make /login to load synchronously, but I don't like this approach as I might be adding more "public" routes that should render in LayoutBlank, and same glitch also happens on them even when entering directly and not being redirected by the router.
Is there any fix to this other than switching the order of layouts so blank is default or loading everything synchronously? I tried to use another beforeEach hook instead of watching route.meta.layout but all it did, was moving the glitch to route leaving instead of entering when layouts were switched.
I couldn't use Vue3 in snippet so I put a simple demo code here:
https://stackblitz.com/edit/vue3-vue-router-meta-layout-spc5tq?file=src%2Flayouts%2FAppLayout.vue,src%2Frouter.ts
When you reload the web container, you will notice the red "Default Layout" text flicker.
Some code:
<script setup lang="ts">
import AppLayoutDefault from './AppLayoutDefault.vue'
import AppLayoutBlank from './AppLayoutBlank.vue'
import { markRaw, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
const layout = ref()
const route = useRoute()
watch(
() => route.meta?.layout as string | undefined,
(metaLayout) => {
layout.value = markRaw(metaLayout === 'blank' ? AppLayoutBlank : AppLayoutDefault)
},
{ immediate: true }
)
</script>
<template>
<component :is="layout"> <router-view /> </component>
</template>
import { createRouter, createWebHistory } from 'vue-router'
import Home from '#/views/Home.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: Home,
},
{
path: '/login',
component: () => import('#/views/Login.vue'),
meta: { layout: 'blank', public: true },
},
{
path: '/admin',
component: () => import('#/views/Admin.vue'),
meta: { layout: 'AppLayoutAdmin' },
},
],
})
let isLoggedIn = false
router.beforeEach((to) => {
if (!to.meta.public && !isLoggedIn) {
isLoggedIn = true
return 'login'
}
})
export default router
I have actually found an answer! It was caused by most examples on the internet not ever mentioning router.isReady()! I was curious why route.fullPath returned / on every page load, even when entering /login. This led me to this snippet:
router.isReady().then(() => app.mount())
And guess what, delaying app.mount() like that actually fixed my whole problem. Now the first layout that is rendered is actually the one configured in the route, even if it's not the default one.

How do I router-link to the same page with an updated query & reload the page? [duplicate]

As part of my Quasar app, I have the following route:
import { RouteRecordRaw} from 'vue-router'
import { uid } from 'quasar'
const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: () => {
console.log('matched /')
return {path: `/${uid()}`}
}
},
{
path: '/:uuid',
component: () => import('pages/User.vue')
// component: User,
},
];
export default routes;
This works fine when going to /: the URL is changed to /73a219e5-2cf2-4dd0-8... and User.vue is executed (specifically there a fetch inside that retrieves some data based on the :uuid parameter.
If I force a route from within a component (User.vue for instance), via
import { useRouter } from 'vue-router'
const router = useRouter()
router.push('/')
I do see that the URL changes to a new UUID but User.vue is not executed. Specifically, a reference to route.params.uuid where const route = useRoute() is not reactive.
Is this normal (= I have to look for anther way to trigger), or is there a misuse (erroneous use) on my side?
The core of the issue is that you're (re)using the same component for rendering the page you're navigating from and the page you're navigating to.
By design, Vue optimises DOM rendering and will reuse the existing component instance. This means certain hooks won't be triggered (e.g: mounted, created, etc...) when changing route.
To force Vue into creating a different component instance when the route changes, use the current route's .fullPath as key on <router-view>:
<template>
...
<router-view :key="route.fullPath"></router-view>
...
</template>
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute();
</script>

vuejs loading component dynamically depending on route component

I'm a newbie to Vuejs and I'm wondering how I can load the component to App.vue depending on the route visited.
In my router -> index.js I have:
import { createRouter, createWebHistory} from 'vue-router'
import Events from '../views/Events.vue'
const routes = [
{
path: '/',
name: 'events',
component: Events
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
In my App.vue I have a template that I want to use for the entire website, so I thought to load the template here instead of loading it into each component.
My template has 1 box in the center of the screen, so the content changes depending on which route the user goes to.
Instead of calling the component content from the routes file, is there a better way to load it dynamically into App.vue depending on the route visited?
I could hard code it in App.vue with something like:
<template>
<Events />
</template>
<script>
import Events from './views/Events.vue'
export default defineComponent({
components: {
Events
},
</script>
But that won't solve my problem about the Events component being loaded dynamically depending on which route is visited. Is there a way to access the underlying component with something like this.$route to load the component instead of hard coding it.
The router-view component will render the component based on its path :
<template>
<router-view/>
</template>
<script>
export default defineComponent({
})
</script>

Conditionally import a component in Vue Router

I'd like to conditionnaly import a component in the vue router. Here is what I have for the moment:
children: [
{
path: ':option',
component: () => import('../components/Option1.vue'),
},
],
Depending on what :option is, I want to import a different component (Option1.vue, Option2.vue, etc.). I know I could put several children but i actually need the option variable in my parent component (I make tests if the route has an option).
How would it be possible to do that?
Thanks in advance :)
You can create a loader component containing a dynamic component instead of doing conditional routing. In the loader, you'll conditionally lazy load the option component based on the route param. Not only is this easier when routing, you also don't have to manually import anything, and only options that are used will be imported.
Step 1. Route to the option loader component
router
{
path: ':option',
component: () => import('../components/OptionLoader.vue'),
}
Step 2. In that option loader template, use a dynamic component which will be determined by a computed called optionComponent:
OptionLoader.vue
<template>
<component :is="optionComponent" />
</template>
Step 3. Create a computed that lazy loads the current option
OptionLoader.vue
export default {
computed: {
optionComponent() {
return () => import(`#/components/Option${this.$route.params.option}.vue`);
}
}
}
This will load the component called "Option5.vue", for example, when the option route param is 5. Now you have a lazy loaded option loader and didn't have to manually import each option.
Edit: OP has now indicated that he's using Vue 3.
Vue 3
For Vue 3, change the computed to use defineAsyncComponent:
OptionsLoader.vue
import { defineAsyncComponent } from "vue";
computed: {
optionComponent() {
return defineAsyncComponent(() =>
import(`#/components/Option${this.$route.params.option}.vue`)
);
}
}
Here is something that works in VueJS3:
<template>
<component :is="userComponent"/>
</template>
<script>
import { defineAsyncComponent } from 'vue';
import { useRoute, useRouter } from 'vue-router';
export default {
computed: {
userComponent() {
const route = useRoute();
const router = useRouter();
const components = {
first: 'Option1',
second: 'Option2',
third: 'OtherOption',
fourth: 'DefaultOption',
};
if (components[route.params.option]) {
return defineAsyncComponent(() => import(`./options/${components[route.params.option]}.vue`));
}
router.push({ path: `/rubrique/${route.params.parent}`, replace: true });
return false;
},
},
};
</script>
Source: https://v3-migration.vuejs.org/breaking-changes/async-components.html
And it's possible to get an error message like this one for the line with "return":
Syntax Error: TypeError: Cannot read property 'range' of null
In that case, it means you probably want to migrate from babel-eslint to #babel/eslint-parser (source: https://babeljs.io/blog/2020/07/13/the-state-of-babel-eslint#the-present)

Vue-router reloads page and I lose my state, how do i avoid this?

I have a form divided in 5 components and the user can navigate through them via steppers (I'm using vue-material for my project). I use vue-router for that. However, I'm having a serious issue here: components lose all the information in the store (I'm using vuex) when they come back to a route they already filled. So to make it clear: if a user fills the first step of the form and then goes to step two, when he wants to come back to step one data is no longer available and the form is totally empty (and the state in vuex is also reset). What am i doing wrong?
import Vue from 'vue'
import Router from 'vue-router'
import Projet from '#/components/Fiches/Projet/Projet'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Home',
component: Projet
},
//other routes here
]
})
And this is the html code
<template>
<div class="project-steppers">
<md-steppers md-dynamic-height md-alternative>
<md-step id="first" to="/Projet" md-label="Projet" />
// other steps here
</md-steppers>
</div>
</template>
And an example of one of the inputs I use:
<md-field>
<label for="project-name">Nom du projet</label>
<md-input id="project-name"
v-model="project.projectName"
name="project-name"
#change="updateProjectName"/>
</md-field>
[...]
methods: {
updateProjectName () {
this.$store.commit(projectStore.MUTATE_PROJECTNAME, this.project.projectName)
}
More information: when I fill the different inputs I see that the store is updated with the new values, so the mutation is working.
First of all, Vuex does not store data in the browser - just in memory. That means that you could either install a third party plugin such as vuex persisted state or write your own methods to set and get the items from your storage, e.g.:
const storage = localStorage.getItem('key');
new Vuex({
state: {
yourProp: storage ?
? JSON.parse(storage.yourDataKey)
: 'default-value'
},
actions: {...}
mutations: {...}
})
I think to should use router-link or $router.push().
Vue:
import Vue from 'vue'
import Router from 'vue-router'
import Projet1 from '#/components/Fiches/Projet/Projet1'
import Projet2 from '#/components/Fiches/Projet/Projet2'
import Projets from '#/components/Fiches/Projet/Projet' //with props
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Home',
component: Projet1 // default project
},
{
path: '/Projet1', // url for the same component
name: 'Projet1',
component: Projet1
},
{
path: '/Project2',
name: 'Projet2', // url for the another component
component: Projet2
},
{
path: '/Project/:id',
name: 'Projets', // url for a component with props
component: Projet,
props: true
}
]
})
HTML: A way to call Projet without reloading with router-link
<template>
<router-link to="/Home"></router>
<router-link to="/Projet1"></router>
<router-link to="/Projet2"></router>
</template>
js: I would add a router push
updateProjectName () {
this.$store.commit(projectStore.MUTATE_PROJECTNAME, this.project.projectName)
this.$router.push('/' + this.project.projectName)
}
Your question looks like the issue opened by kristianmandrup:
menu or tabs with router links!?