Vuejs component reuse on navigation change - clarification sought - vue.js

In the following example, if the user navigates from /user/foo to /another_page and then to /user/bar, will the User component be reused?
const User = {
template: '<div>User</div>'
}
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User }
]
})
The Vue Router manuals says ...
One thing to note when using routes with params is that when the user navigates from /user/foo to /user/bar, the same component instance will be reused. Since both routes render the same component, this is more efficient than destroying the old instance and then creating a new one. However, this also means that the lifecycle hooks of the component will not be called.
Dynamic Route Matching
... but it's not clear whether the component is only reused when there are no intermediary navigation steps. Is there any documentation that discusses component reuse in this kind of detail?
Thanks!

If you navigate from /user/foo to /user/bar then the component will be reused.
This is often used when displaying product page for example. Note that there are ways to rebuild component if it is needed.
If you will navigate from /user/foo to /another_page and then navigate to /user/bar then your component will be destroyed when leaving to /another_page and created when navigating to /user/bar.
To sum up:
/user/foo -> /another_page - component gets destroyed
/user/foo -> /user/bar - component will be reused

Related

Why does the browser display cached Vue.js view on route/url change?

I have a homepage with <router-link> tags to views. It is a simple master/detail relationship where the Homepage is a catalogue of products and the Product detail page/view shows information on each item.
When I first launch the website and click on an item on the Homepage view (e.g. URL: http://localhost:8080/100-sql-server-2019-licence), the Product view gets loaded and the product detail loads fine.
If I then press the back button in the browser to return to the Homepage and then click on a different Product (e.g. URL: http://localhost:8080/101-oracle-12c-licence), the URL in the browser address bar changes but I get the previous product's information. Its lightning quick and no new network calls are done which means its just showing a cached page of the previous product. If I then hit the refresh button while on that page, the network call is made and the correct product information is displayed.
I did a search online but couldn't find this problem described on the search results. Could anyone point me in the right direction of how to cause a refresh/re-render of a route when the route changes?
What is happening
vue-router will cache your components by default.
So when you navigate to the second product (that probably renders the same component as the first product), the component will not be instantiated again for performance reasons.
From the vue-router documentation:
For example, for a route with dynamic params /foo/:id, when we
navigate between /foo/1 and /foo/2, the same Foo component instance
will be reused.
The easy (but dirty) fix
The easy -but hacky and not recommended - way to solve this is to give your <router-view /> a key property, e.g.:
<router-view :key="$route.fullPath" />
This will force vue-router to re-instantiate the view component every time the url changes.
However you will loose all performance benefits you would normally get from the caching.
Clean fix: properly handling route changes
The clean way to solve this problem is to react to the route-change in your component (mostly this boils down to moving ajax calls from mounted into a $route watcher), e.g.:
<script>
export default {
data() {
return {
productDetails: null,
loading: false
};
},
watch: {
'$route': {
// with immediate handler gets called on first mount aswell
immediate: true,
// handler will be called every time the route changes.
// reset your local component state and fetch the new data you need here.
async handler(route) {
this.loading = true;
this.productDetails = null;
try {
// example for fetching your product data
const res = await fetch("http://give.me.product.data/" + encodeURIComponent(route.params.id));
this.productDetails = await res.json();
} finally {
this.loading = false;
}
}
}
}
};
</script>
Alternative: Navigation Guards
Alternatively you could also use vue-routers In-Component Navigation Guards to react to route changes:
<script>
export default {
async beforeRouteUpdate (to, from, next) {
// TODO: The route has changed.
// The old route is in `from`, the new route in `to`.
this.productData = await getProductDataFromSomewhere();
// route will not change before you haven't called `next()`
next();
}
};
</script>
The downside of the navigation guards is that you can only use them directly in the component that the route renders.
So you can't use navigation guards in components deeper within the hierarchy.
The upside is that the browser will not view your site before you call next(), which gives you time to load the data necessary before your route is displayed.
Some helpful ressources
Vue Router Navigation Guards Documentation
vue-router github issue
Similar Question about vue-router component reuse on stackoverflow

why parent component never render until child router is complete?

In my vue application I have route inside route. the problem is until the inner route is resolved then the outer/parent route is display.
Here is how I defined the routes:
const router = new VueRouter({
// Use the HTML5 History API (fallback to URL hash if unsupported)
mode: "history",
routes: [
{
path: "/",
name: "base",
component: Base,
children: [{ path: "", name: "home", component: Home }]
}
]
});
The problem is vue is waiting for beforeRouteEnter to complete then it show the Home and Base.
If I remove the next() from beforeRouterEnter in the Home the component base is never display.
This is a problem because Home can take a lot of time to load data meanwhile base should be render to screen (base has toolbar for example).
Here is example of the problem
How can I solve this issue?
Sometimes you need to fetch data from the server when a route is
activated. We can achieve this in two different ways:
Fetching After Navigation: perform the navigation first, and fetch
data in the incoming component's lifecycle hook. Display a loading
state while data is being fetched.
Fetching Before Navigation: Fetch data before navigation in the
route enter guard, and perform the navigation after data has been
fetched.
Since your fetch takes too much time, your preferable approach would be the first aforementioned. Jest perform your API calls in mounted/created hooks of Home.vue component.

Vue router reloading the current route

Without reloading the whole page I need to reload the current route again (Only a component reload) in a vue app.
I am having a path in vue router like below,
{
path: "/dashboard",
name: "dashboard",
component: loadView("Dashboard"),
},
When user clicks on the Dashboard navigation item user will be redirected to the Dashboard page with vue router programmatic navigation
this.$router.push({ name: "dashboard" });
But when user already in the dashboard route and user clicks the Dashboard nav item again nothing happens. I think this is vue router's default behaviour. But I need to force reload the Dashboard component (Not to refresh the whole page).
I can't use beforeRouteUpdate since the router is not updated. Also I have tried the global before guards like beforeEach. But it is also not working.
How can I force reload the dashboard component without reloading the whole page?
It can be done in two ways.
1) Try doing vm.$forceUpdate(); as suggested here.
2) You can take the strategy of assigning keys to children, but whenever you want to re-render a component, you just update the key.
<template>
<component-to-re-render :key="componentKey" />
</template>
<script>
export default {
data() {
return {
componentKey: 0,
};
},
methods: {
forceRerender() {
this.componentKey += 1;
}
}
}
</script>
Every time that forceRerender is called, the prop componentKey will change. When this happens, Vue will know that it has to destroy the component and create a new one.
What you get is a child component that will re-initialize itself and “reset” its state.
Not mentioned here, but as the offered solutions require a lot of additional work just to get the app to render correctly, which imo is a brittle solution.. we have just implemented another solution which works quite well..
Although it is a total hack.
if (this.$route.name === redirect.name) {
// this is a filthy hack - the vue router will not reload the current page and then have vue update the view.
// This hack routes to a generic page, then after this has happened the real redirect can happen
// It happens on most devices too fast to be noticed by the human eye, and in addition does not do a window
// redirect which breaks the mobile apps.
await this.$router.push({
name: RouteNames.ROUTE_REDIRECT_PLACEHOLDER
});
}
... now continue to do your normal redirect.
Essentially, redirect to a placeholder, await the response but then immediately continue to another page you actually wanted to move toward

Declarative router link not clearing model in Vue component using VueRouter

Developing a task scheduler the path '/task?id=10' fills the component model with an async response with the task information, that works ok.
In the navigation bar I have the following router link:
<router-link to="/task">New Task</router-link>
So I am using the same path and component for creating a new task and editing existing ones all based in if "id" parameter is present or not.
The problem is that if I am in path '/task?id=10' and I fill some model fields, then I click the router-link pointing to '/task' (no params) it changes the browser URL but it does not clear the component model so the input data persists.
How can I restart/reload the component when landing it through a declarative router-link?
You can make parent-child component. Parent component is on /task route, child would be on '/task/10. More, Nested Routes. Also, you don't need to append '?id=', just /task/10.
const router = new VueRouter({
routes: [
{ path: '/task', component: Task,
children: [
{ path: ':id', component: TaskId }
]
}]
});
jsfiddle

Precedence in lifecycle hooks with Vue.js and Vue-router

I'm building an app with vue and vue-router. In some routes, I need to check some conditions first, if those conditions are not satisfied, then redirect to another component, so I used the activate hook on the router option of my component, and it works fine. Also, inside that same component, I have the vue created hook to load some data, the thing is that if those conditions that I mentioned before are not met, then I can't load the data in the created hook. What I would expect is that if that condition is not met, and the redirect hook was called, then the created hook wont get triggered, but what is actually happening is that whene that condition is false, then the redirect of the activate hook get calledn and also the created hook from Vue. So, more than a solution for my particular use case, I would like to know the order of execution of the hooks when using vue and vue router together.
For Vue 2.0:
beforeCreate
created
beforeMount
mounted
beforeUpdate
updated
beforeDestroy
destroyed
Now when using vue-router 2.0, the data fetching can be done at two places, as per their doc:
Fetching After Navigation: perform the navigation first, and fetch
data in the incoming component's lifecycle hook. Display a loading
state while data is being fetched.
Fetching Before Navigation: Fetch data before navigation in the route
enter guard, and perform the navigation after data has been fetched.
For your case, you can write you data fetching logic in a function and call it inside the "created" hook of the component lifecylce. If at all the data changes with the route, then write a watcher on the $route object, which will trigger your data fetching function.
As the data hook of vue-router 0.7 is deprecated and instead the $route object has been made reactive. Read more here.
Maybe you are interested in In-Component Guards (additional hooks available in components loaded using Vue Router)
const Foo = {
template: `...`,
beforeRouteEnter (to, from, next) {
// called before the route that renders this component is confirmed.
// does NOT have access to `this` component instance,
// because it has not been created yet when this guard is called!
},
beforeRouteUpdate (to, from, next) {
// called when the route that renders this component has changed,
// but this component is reused in the new route.
// For example, for a route with dynamic params `/foo/:id`, when we
// navigate between `/foo/1` and `/foo/2`, the same `Foo` component instance
// will be reused, and this hook will be called when that happens.
// has access to `this` component instance.
},
beforeRouteLeave (to, from, next) {
// called when the route that renders this component is about to
// be navigated away from.
// has access to `this` component instance.
}
}
https://router.vuejs.org/guide/advanced/navigation-guards.html#in-component-guards
If you use Vue + Vue Router and you are at RouteA and navigates from it to RouteB and each route/component register for something (e.g. receiving data supported by root) on "created" and unregister on "beforeDestroy" but when you leave RouteA its "beforeDestroy" is called after RouteB "created" so you have nothing registered! I have tested it and VUE 1 had correct order. It must be changed somewhere in VUE 2 + VUE Router 2.
Correct/expected hooks order in VUE 1 when going from RouteA to RouteB:
RouteA beforeDestroy
RouteB created
Incorrect/unexpected hooks order in VUE 2 when going from RouteA to RouteB:
RouteB created
RouteA beforeDestroy
Solution is to use "created" + "beforeRouteLeave" for VUE 2 + VUE Router 2.
Why not add console logs to each and see for yourself?
As far as I can tell without testing:
canReuse
canActivate
-- now the component instance is being created:
created
beforeCompile
compiled
-- (not sure wheither ready or activate comes first, but i guess ready)
ready
activate
data
For your particular case, the data fetching should happen in the data hook - that's what it's for, after all.