I've implemented component routing based on data returned from a server page held in a CMS. Summary - on a route change the path is sent to the server which returns a template name and data for that template. The template name maps to a component name which then uses the data. The issue I am seeing is that after I change the routing, while the target component is created and displayed, the original component (source route) is also created twice.
Here's the code that fetches data from the server and uses it to set the new route.
router.beforeEach((to, from, next) => {
if (!app) {
next()
return
}
// get page data from the server.
app.$store.dispatch('getPageData', to.fullPath).then(r => {
// check whether vue-router knows this route
// if not then map it to the right component
if (to.matched.length === 0) {
let component = mapTemplate(r.data.page.template)
// add the route (undocumented but intentionally released)
router.addRoutes([{path: to.path, component}])
// change to the same route now that vue-router knows it
next(to.fullPath)
return
}
next()
})
})
Now the (abridged) console log. The sequence captured in the log is:
load '/' which uses template p-home.vue.
navigate using menu to /ready-to-wear/ which uses component p-rtw-products
The bold lines show p-home lifecycle hooks. The italic line shows when vue-router is done with the routing. The line that says "setting route" displays the new route that was added to the router.
p-home.vue?1e58:54 p-home created
p-home.vue?1e58:54 p-home mounted
lmd-app.vue?70d9:48 fetchdata for /
index.js?3672:62 router.beforeEach() / => /ready-to-wear/
p-home.vue?1e58:54 p-home destroyed
p-home.vue?1e58:54 p-home created
p-home.vue?1e58:54 p-home mounted
index.js?3672:73 beforeEach() setting route /ready-to-wear/ target: PRtwProducts
index.js?3672:62 router.beforeEach() / => /ready-to-wear/
p-home.vue?1e58:54 p-home destroyed
p-home.vue?1e58:54 p-home created
p-home.vue?1e58:54 p-home mounted
index.js?3672:77 router.afterEach()
p-rtw-products.vue?5a57:22 p-rtw-products created
p-home.vue?1e58:54 p-home destroyed
p-rtw-products.vue?5a57:25 p-rtw-products mounted
OK, thanks for all that have gotten this far. I'd expect one create-destroy sequence for p-home yet there are three. I can even imagine that because it was the original source of the route that creating it was already queued (though I don't really believe it).
Does anyone know why it is behaving as it does?
Related
I see this question has been asked a few times on here, but none of the answers have really helped me in this current situation.
I have an app I'm working on with a sidebar with tabs that link to different dashboards. Each of the SidebarLinks are a router-link with the to key being fed the route prop from the main component.
Inside one of these dashboards, the Analysis dashboard, there is another router that routes you to child routes for specific Analyses with their own ids (EX: /analysis/1).
The user clicks on a button for a specific analysis and they are routed to a page containing that information, on the same page.
The Error
When I click the Analysis SidebarLink the route in the url changes back to /analysis, but the page doesn't update/refresh.
I don't get an error in the console, but I do get the failure in the devtools.
I understand that Vue Router doesn't route back to a route you are already on, but I need it to. If you refresh the page when the url is just /analysis it routes back to it's inital state.
Is there anyway to refresh when it rereoutes to /analysis? Or a way to handle this error to work as intended?
What I've tried
I've tried changing the router-link to an <a> tag and programatically use router.push and then catch the error, but that doesn't do anything.
I've tried checking if the route.fullPath.contains("/analysis") and then just do router.back() but that doesn't seem to work either.
SidebarLink router function
function goToRoute() {
console.log(`route.fullPath → `, route.fullPath)
if (route.fullPath.match('/analysis*') as any) {
console.log('route includes /analysis')
router.back()
} else {
console.log('route doesnt inclue /analysis')
router
.push({
path: props.route,
})
.catch(() => {})
}
}
Inital /analysis Page
This is what the page looks like normally
/analysis/1 Page
This is what the route to analysis/1 looks like (url changes)
/analysis/1 Page When Issue Analysis SidebarLink Clicked
This is what the route to analysis looks like when the sidebarlink is clicked (url changes, but the page stays the same)
I suspect you are fetching your data from a backend service or data files
If yes you can refetch the data everytime the route param changed by watching it.
watch: {
'$route.params.id': function (id) {
if(id)
this.$store.dispatch('fetchOneAnalys', id)
else
this.$store.dispatch('fetchAllAnalyses')
}
I have a Vue app which does a little localStorage and server check on app load, to determine where to initially route the user.
This is in the App's main entry component, in the created() hook
My problem is that the default / route's Component visibly loads first, then the server call and everything happens which causes the user the route to their correct location
How can I delay the rendering of the initial component until my app's main component created() method completes, and then purposely navigates the user to the correct route?
I had this problem before and I firmly believe that you must have the initial files for your routes and your router configuration.
In the configuration, you could handle the permission and router before each route and with next() . In the router file, you can set your params and check them in the index.js file(router configuration)
you could also use your localStorage data in Router.beforeeach
EDIT: I just saw you used the created method... like mentioned below use beforeRouteEnter instead with the next() parameter it provides
First of all I wouldn't recommend using a delay but instead a variable that keeps track if the API call is done or not. You can achieve this using the mounted method:
data() {
return {
loaded: false,
}
}
async mounted() {
await yourAPICALL()
if (checkIfTokenIsOkay) {
return this.loaded = true;
}
// do something here when token is false
}
Now in your html only show it when loaded it true:
<div v-if="loaded">
// html
</div>
An better approuch is using the beforeRouteEnter method which allows you to not even load the page instead of not showing it: https://router.vuejs.org/guide/advanced/navigation-guards.html
I am pushing query parameter for page change it works fine:
getProducts(){
this.$router
.push({
name: 'products',
query: {
page: this.page,
},
})
.catch(() => {})
... fetching data from backend
}
It works fine when I just click pagination items and data is loading correctly but when I click back from browser query param is geting changed but pagination and data doesn't can't changed because this.page value remains the same. How can be this fixed?
You will have to add a befoureRouteUpdate(to, from, next) hook in your component - or a watcher on $route. Vue-Router reuses the same component because you are not changing the route - only the query parameters.
Keep in mind that:
the beforeRouteUpdate will be called only when the component is being reused - not when the route is visited for the first time (for the latter you need beforeRouteEnter)
the watcher will be also called when you leave the route (e.g. if you go to clients page)
I am trialling a project in Nuxt. Liking it so far except I am a little confused as to how to load data from an external async service so that it is available in Vuex from the very first route.
I have tried adding middleware on the default layout to dispatch the store action but I do not see the service being called straight away. Only when I navigate deeper into the routes do I see the action dispatched.
I did something similar in a standard Vue project and added the created method to the App.vue.
Is there a similar way in Nuxt?
What you need is called a fetch.
The fetch method, if set, is called every time before loading the component (only for page components). It will be called server-side once (on the first request to the Nuxt app) and client-side when navigating to further routes.
Warning: You don't have access of the component instance through this inside fetch because it is called before initiating the component.
async fetch({ store }) {
await store.dispatch('your-action')
}
If you need parameter:
async fetch({ store, params }) {
await store.dispatch('your-action', params.id)
}
I gave an example of id. The name of the parameter depends on the name of your page.
_id => params.id
_slug => parmas.slug
...
I have a ClientManagePage where I display client information and allow for the removal of the displayed client.
The vue-router route configuration for that page looks like this:
{
path: '/client/:id/manage',
name: 'client',
component: ClientManagePage,
props: ({ params }) => ({ id: params.id }),
}
The client entities are stored in a vuex store. ClientManagePage gets its client entity from the store using the id prop and displays various properties of the client and a "remove" button.
The remove button listener is (inside a mapActions):
async removeClientClicked(dispatch) {
// Wait for the action to complete before navigating to the client list
// because otherwise the ClientListPage might fetch the client list before
// this client is actually deleted on the backend and display it again.
await dispatch('removeClientAction', this.id);
this.$router.push({ name: 'clientList' });
},
The vuex action that removes a client is:
async function removeClientAction({ commit }, id) {
// Remove the client from the store first (optimistic removal)
commit('removeClient', id);
// Actually remove the client on the backend
await api.remove('clients', id);
// Moving "commit('removeClient', id);" here still produces the warning mentioned below
}
My problem is how to handle navigating to the other route when removing a client. The current code produces warnings in development mode such as:
[Vue warn]: Error in render: "TypeError: Cannot read property 'name' of undefined"
found in
---> <ClientManagePage> at src/pages/ClientManagePage.vue
<Root>
This is of course caused by the reactivity system kicking in and trying to update the content of the page with the now-deleted vuex client entity. This happens before the removeClientAction is completed therefore the navigation to the ClientList page.
I've come up with some possible solutions to this, but they are not very appealing:
Have a v-if="client" at the top of the ClientManagePage that hides everything while the client does not exist in the store.
Use the computed property client in ClientManagePage to return a default "dummy" client that contains the required properties for the page. The page will still flash with "fake" content while the action is underway though.
Navigate to "clientList" right after (or even before) dispatching removeClientAction. This causes the clientList to display the removed client briefly while the action completes which is not good.
Are there other solutions to this seemingly common problem of navigating away when deleting the underlying vuex entity displayed on the current page?
I ended up doing a big v-if at the top of the ClientManagePage that hides everything while the client does not exist in the store. It's not pretty, but it works. An improvement could be to display a "please wait, operation in progress" in v-else.
One option is to externalize the deletion of the record. There are a number of ways to do that, but the simplest for me was to create a new route, /records/delete/:id, and place a route guard on that route that triggers the removal. Then redirect to the records list where you wanted to go in the first place. Something along the lines of:
import store from "wherever/your/store/is";
const routes = [{
path: "/records/delete/:id",
name: "deleteRecord",
props: true,
beforeEnter: (to, from, next) => {
store.dispatch("DELETE_RECORD", to.params.id).then(() => console.log("record deleted!"));
next({name: "some/path/you/wanted/to/go/to"});
}
}, ...];