Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Vue 3 shows previous route in onMounted

I'm using Vue with a dynamic navigation based on my backend. An endpoint decides what page to display based on the current route.

I've made a simplified version below with the relevant parts. Everything works fine, except for when I use a <router-link> or router.push to one of my routes. In the onMounted callback of my target page, the route seems to be the previous route (see AboutView.vue below).

I've tried await router.isReady(), but as far as I can tell it's not relevant here, it's only used for an initial page view.

Sample repo built with node 16.13.2: https://github.com/tietoevry-johan/vue-routing

App.vue

<template>
  <router-view/>
</template>

router.ts

export const currentPage = ref('HomeView')

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      name: "PageSelector",
      path: "/:catchAll(.*)",
      component: PageSelector
    }
  ]
});

router.beforeEach(async (to, _, next) => {
  currentPage.value = await getComponentByPath(to.path)
  next()
})

async function getComponentByPath(path: string) {
  return path === '/' ? 'HomeView' : 'AboutView'
}

PageSelector.vue

<template>
  <component :is="currentPage" />
</template>

<script lang="ts">
import { defineComponent } from "vue"
import AboutView from "@/views/AboutView.vue"
import HomeView from "@/views/HomeView.vue"
import { currentPage } from "@/router"

export default defineComponent({
  // eslint-disable-next-line vue/no-unused-components
  components: { HomeView, AboutView },
  setup() {
    return {
      currentPage
    }
  }
})
</script>

AboutView.vue

<template>
  about
</template>

<script lang="ts">
import router from "@/router";
import { defineComponent, onMounted } from "vue";

export default defineComponent({
  setup() {
    onMounted(() => {
      console.log(router.currentRoute.value?.fullPath) // prints / instead of /about
      // when clicking a router link or router push to this page
    })
  }
})

</script>
like image 780
Johan Avatar asked Oct 21 '25 00:10

Johan


1 Answers

The problem comes from navigating to the current route, which updates current route parameters, but doesn't trigger a reload of the component in <router-view>, while a side-effect changes how the component behaves. The side effect is triggered in the router's beforeEach(), and apparently the update in the component is handled before the current route parameters are updated.


The easiest fix is the one @EstusFlask recommended in the comments, just use afterEach() instead of beforeEach():

router.afterEach(async (to) => {
  currentPage.value = await getComponentByPath(to.path)
})

This will update the variable after current route has been updated, giving you the desired order of events.

playground using afterEach()


A more robust approach is to not mix router functionality and component functionality. For this, you add a prop to your PageSelector, which determines the rendered page:

<template>
  <component :is="currentPage ?? 'HomeView'" />
</template>

<script lang="ts">
import { defineComponent } from "vue";
import AboutView from "./AboutView.vue";
import HomeView from "./HomeView.vue";

export default defineComponent({
  components: { HomeView, AboutView },
  props: ["currentPage"],
});
</script>

Now you can remove the beforeEach/afterEach hook, and instead pass in the prop through the route:

export const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      name: "PageSelector",
      path: "/:catchAll(.*)",
      component: PageSelector,
      props: (route) => ({
        currentPage: getComponentByPath(route.path),
      }),
    },
  ],
});

Now you can use regular props instead of the hidden state variable.

playground using router props

Might be worth thinking about if matching string to component should happen in PageSelector instead of a function in router (getComponentByPath()), as you probably want to avoid running PageSelector without a matched component. In your question, getComponentByPath() is declared async, despite not doing anything asynchronously - if this is not a mistake, moving it into the component will allow you to handle the async event with more control.

playground with component matching in PageSelector


Expanding on the above approach, you could also make PageSelector a transient page, that just resolves the actual route for the given props and then redirects to the actual page, which are also defined routes:

export const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      name: "PageSelector",
      path: "/loading/:pageId",
      component: PageSelector,
      props: true
    },
    {
      name: "HomeView",
      path: "/home",
      component: HomeView
    },
    ...
  ],
});

The PageSelector component shows a loading screen while the actual route is resolved and then redirects to it. All necessary data can be passed as route params. The advantage here is that PageSelector can focus on complex things (like error handling), but routes themselves work independently and loading can be skipped if not necessary.

playground where PageSelector uses redirect

like image 198
Moritz Ringler Avatar answered Oct 22 '25 14:10

Moritz Ringler