I would like to know how can I stop component reusing in Vue-router.
I'm building a simple page application and I am unable to update data after clicking the same link twice.
Is it possible to somehow force reloading or what are the best practices in my situation?
Use the key attribute on router-view set to current url. It's built in, so no need to write any code.
<router-view :key="$route.fullPath"></router-view>
Vue Router reuses the same component therefore the mounted hook won't be called. As stated in the documentation:
The same component instance will be reused [...] the lifecycle hooks of the component will not be called.
If you want to update the data you have two options:
Watch the $route object
const User = {
template: '...',
watch: {
'$route' (to, from) {
// react to route changes...
}
}
}
Use the beforeRouteUpdate navigation guard
const User = {
template: '...',
beforeRouteUpdate (to, from, next) {
// react to route changes...
// don't forget to call next()
}
}
For a more detailed explanation you can check the section Reacting to Param Changes of the Vue Router documentation: https://router.vuejs.org/guide/essentials/dynamic-matching.html#reacting-to-params-changes.
One way to do this is to put a key on the router-view and append a timestamp querystring to your router-link
const Home = {
template: '<div>Home</div>',
created() {
console.log('This should log everytime you click home.');
},
};
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/', component: Home },
]
});
new Vue({
router,
el: '#app',
});
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<div id="app">
<router-link :to="`/?q=${Date.now()}`">/home</router-link>
<router-view :key="$route.fullPath"></router-view>
</div>
One reason not to do it this way is because it'll force rerenders on components that you may want to be reused such as on routes like
/posts/1
/posts/2
Related
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>
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>
I'm trying to use vue-router based on the examples, such as
let routes = [
{ path: '/', component: MainComponent },
];
let router = new VueRouter({routes});
new Vue({ router }).$mount('#app');
but I always get this error:
vue.runtime.esm.js:619 [Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available.
Can I fix this by using the render function? I tried,
let routes = [];
But still fails.
Ok, I spent a half day on this and I finally got it working: vue-router + webpack + runtime-only vue.
This tutorial was the most helpful. What I learned:
If you use vue-cli, the vue version is in the webpack.base.conf.js
vue.esm.js will include the compiler
vue.runtime.esm.js will NOT include the compiler
If you want to use runtime, you must change your main.js. Do NOT use this
new Vue({
el: '#app',
router,
template: '<App/>', // <== This is bad
components: { App }
});
and instead DO use this
Vue.use(VueRouter); // <== very important
new Vue({
router,
render(createElement) {
return createElement(App);
}
}).$mount('#app');
You can use either $mount or the el: with runtime. Both work, but the $mount gives you more flexibility. And of course the router is created the usual way
let routes = [
{ path: '/', component: MainComponent },
];
let router = new VueRouter({ routes });
And if you are still seeing the error
[Vue warn]: You are using the runtime-only build of Vue
in the console, then make double sure that you never ever in your code use any templates with strings. Even inside your *.vue files, if you try to do
const Foo = { template: '<div>foo</div>' };
it will fail. Instead you must either use <template> tags, or else you must use createElement. Good luck!
The accepted solution is right in that you cannot use string template with runtime only Vue, and you have to implement the render function. However, if you want to render components at the top level instead of using router-view, you can get the matched component and render it manually:
const routes = [
{ path: '/about', component: About },
{ path: '/index', component: App },
{ path: "*", component: App }
]
new Vue({
router,
render: h => {
return h(router.getMatchedComponents()[0])
},
}).$mount('#app')
It appears to be impossible to use the runtime, based on these two MREs (one with, one without runtime). If nothing else, you can use these snippets if you choose to post an issue on their github or the vue forums, or another answer can use them as a template to prove me incorrect. This assumes you're not using vue-cli. With vue-cli you need to opt-in to including the compiler in your builds. See https://cli.vuejs.org/config/#runtimecompiler and https://v2.vuejs.org/v2/guide/installation.html#Runtime-Compiler-vs-Runtime-only
Fails (console warning - vue runtime)
let routes = [
{
path: "",
component: {
render(h) {
return h("div", "hello world");
}
}
}
];
let router = new VueRouter({ routes });
new Vue({ router }).$mount("#app");
<script src="https://unpkg.com/vue/dist/vue.runtime.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<div id="app">
<router-view></router-view>
</div>
Works (no console warning - full vue)
let routes = [
{
path: "",
component: {
render(h) {
return h("div", "hello world");
}
}
}
];
let router = new VueRouter({ routes });
new Vue({ router }).$mount("#app");
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<div id="app">
<router-view></router-view>
</div>
For Vue 3 (here with TypeScript), the trick is to use the h component render function as the result of the setup function as the top-level component instead of the default createApp({}) which appears to be interpreted as a template.
So I have:
const app = createApp({
setup() {
const component = computed((): Component => {
const res = router.currentRoute.value.matched
if (res.length > 0 && res[0].components)
return res[0].components.default
else
return Loading
})
return () => h(component.value)
}
})
...
vue-router's Router exposes a ready-to-render component in property currentRoute.value.matched[0].components.default. Be sure to check for a match and for the component to be loaded (this works for lazy components as well imported using () => import('path/to/Component.vue')).
Obviously, the router is integrated into the app and and the app mounted this way, with the router being created using createRouter:
...
const router = createRouter({
history: createWebHistory(),
routes: [
...
]
})
app.use(router)
app.mount('#app')
In the index note that <router-view/> is not used. Probably was the only thing using the runtime compiler, think about a waste of space as the recommended option in the docs! The little extra effort in the root component to make it static is definitely worth it. The index looks like this:
<!DOCTYPE html>
<html class="dark" data-theme="dark">
<head>
<meta charset="utf-8"/>
<title>Title</title>
<script type="module" src="/src/main.ts"></script>
</head>
<body>
<div id="app">
</div>
</body>
</html>
Tested with Vite, allows to have vue: 'vue/dist/vue.runtime.esm-bundler.js'. Pretty compact!
I have an App component and it renders fine. It's basically a component comprising two components.
App (Vue component):
1. Navigation // a menu bar with menu items
//like <router-link to="/">Home</router-link>
//like <router-link to="/contact">Contact</router-link>
2. Library // a component listing books.
I want to add another component Contact which I tried adding using vue-router, so I use the above navigation component. However, the routes don't seem to affect anything at all. What am I doing wrong?
My main.js file
const Contact = {
template: '<div>Contact Us</div>'
}
const routes = [
{
path: '/',
component: App // any change here also does not make any difference!!!
},
{
path: '/contact',
component: Contact, // this or any component doesnt make any difference!!!
}
];
const router = new VueRouter({
routes,
});
new Vue({
render: h => h(App),
router,
}).$mount('#app')
When using the vue-router with .vue files, there is no documented way to pass data from one view/component to another.
Let's take the following setup...
main.js:
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter);
let routes = [
{
path: '/page1',
component: require('./views/Posts.vue')
},
{
path: '/page2',
component: require('./views/EditPost.vue')
}
];
let router = new VueRouter({
routes
});
new Vue({
el: '#main',
router
});
Posts.vue:
<template>
<div>
Posts.vue passing the ID to EditPost.vue: {{ postId }}
</div>
</template>
<script>
export default {
data() {
return {
allPostsHere: // Whatever...
}
}
}
</script>
EditPost.vue:
<template>
<div>
EditPost.vue received ID from Posts.vue: {{ receivedId }}
</div>
</template>
<script>
export default {
data() {
return {
receivedId: // This is where I need the ID from Posts.vue
}
}
}
</script>
Please note: It is not possible to receive the ID directly from the EditPost.vue, because it has to be selected from Posts.vue.
Question: How can I pass the ID from one view/component to the other?
A route can only be accessed via a URL and a URL has to be something user can type into the URL bar, therefore to pass a variable from one view component to another you have to use route params.
I assume you have a list of posts in Posts component and want to change page to edit a specific post in EditPost component.
The most basic setup would be to add a link in the post list to redirect to the edit page:
<div v-for="post in posts">
{{ post.title }}
<router-link :to="'/post/' + post.id + '/edit'">Edit</router-link>
</div>
Your routes would look like this:
[
{
path: '/posts',
component: require('./views/Posts.vue'),
},
{
path: '/post/:postId/edit',
component: require('./views/EditPost.vue'),
props: true,
},
]
The props configuration option is just to inform the Router to convert route params to component props. For more information see Passing props to route components.
Then in EditPost you'd accept the id and fetch the post from server.
export default {
props: ['postId'],
data() {
return {
post: null,
}
},
mounted() {
this.fetchPost();
},
methods: {
fetchPost() {
axios.get('/api/post/' + this.postId)
.then(response => this.post = response.data);
},
},
}
After the request has been completed, EditPost has its own copy which it can further process.
Note, that on every post edit and every time you enter the post list, you'll make a request to the server which in some cases may be unnecessary, because all needed information is already in the post list and doesn't change between requests. If you want to improve performance in such cases, I'd advise integrating Vuex into your app.
If you decide to do so, the components would look very similar, except instead of fetching the post to edit via an HTTP request, you'd retrieve it from the Vuex store. See Vuex documentation for more information.
if you don't want the params appear in the URL bar,you can use window.sessionStorage, window.localStorage or vuex.
Before you leave the view, set your parameters and get it after entering the new view.
You can use a prop on the <router-view :my-id="parentStoredId"></router-view> to pass down data present in the app.vue (main component). To change the parent data you need to emit a custom event comprising the value, from the childs (Posts.vue, EditPost.vue).
Another way is the Non Parent-Child Communication.
The way I prefer is Vuex. Even if it require you to learn the usage, it will repay back when the app grows.