Vue Router Advanced: Nested Routes, Navigation Guards and Lazy Loading

Advanced
14 min

Vue Router Advanced: Nested Routes, Navigation Guards and Lazy Loading

You can already define routes and navigate between them. Real applications need more: layouts that wrap groups of pages, protected areas that redirect anonymous users, prompts before leaving unsaved forms, and bundles that do not download the admin area for visitors who never open it. After this lesson you will structure nested routes, write global and per-route guards, load route components lazily, and control scroll and transitions.

Nested Routes and Layouts

A route can declare children. The parent component renders a <RouterView> where the matched child appears, which is how shared layouts (sidebar, tabs) are built:

javascript
const routes = [ { path: '/account', component: AccountLayout, children: [ { path: '', name: 'account', component: AccountHome }, // /account { path: 'orders', name: 'orders', component: OrderList }, // /account/orders { path: 'orders/:id', name: 'order', component: OrderView, props: true } ] } ]

Child paths do not start with /; an empty path renders as the default child. props: true passes route params (id) to the component as props, which keeps the view testable without useRoute. Named views (components: { default: Main, sidebar: Sidebar } with <RouterView name="sidebar">) render several components for one route.

Navigation Guards

Guards run before a navigation is confirmed. Returning nothing or true allows it, returning false cancels it, and returning a route location redirects. Guards may be async:

javascript
router.beforeEach(async (to, from) => { const auth = useAuthStore() if (to.meta.requiresAuth && !auth.isLoggedIn) { return { name: 'login', query: { redirect: to.fullPath } } } }) router.afterEach((to) => { document.title = to.meta.title ?? 'My App' })

meta is arbitrary data attached to a route record; to.meta contains the merged meta of the matched parent and child records. The guard order is: leave guards of the old component, beforeEach, per-route beforeEnter, in-component guards, beforeResolve, then afterEach once the navigation is confirmed.

Per-route guards live on the record: beforeEnter: (to) => hasPermission(to.params.id) || '/403'. They run only when entering that route from a different one.

In-Component Guards

Inside <script setup>, two composables handle the cases where the component is already rendered:

vue
<script setup> import { ref } from 'vue' import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router' const dirty = ref(false) onBeforeRouteLeave(() => { if (dirty.value && !window.confirm('Discard unsaved changes?')) return false }) onBeforeRouteUpdate(async (to) => { // same component reused: /orders/1 -> /orders/2 await loadOrder(to.params.id) }) </script>

onBeforeRouteUpdate matters because Vue reuses the component instance when only params change; onMounted will not run again. The alternative, <RouterView :key="$route.fullPath">, forces a remount at the cost of re-fetching everything.

Lazy Loading Routes

Import route components with a dynamic import() so each becomes a separate chunk that downloads on first navigation:

javascript
const routes = [ { path: '/', component: HomeView }, // eager { path: '/reports', component: () => import('@/views/ReportsView.vue') } // lazy ]

Keep the landing page eager and lazy-load everything else. If a deployment replaces chunk files while a user still has an old page open, a lazy import can fail; handle it with router.onError and reload on a module fetch failure.

Scroll Behavior, Transitions and the Catch-All

javascript
const router = createRouter({ history: createWebHistory(), routes, scrollBehavior(to, from, savedPosition) { if (savedPosition) return savedPosition // back/forward if (to.hash) return { el: to.hash, behavior: 'smooth' } return { top: 0 } } })

A catch-all route { path: '/:pathMatch(.*)*', component: NotFound } handles unknown URLs. For page transitions, use the RouterView slot: <RouterView v-slot="{ Component }"><Transition name="fade" mode="out-in"><component :is="Component" /></Transition></RouterView>.

Common mistakes

  • Calling next() in guards; Vue Router 4 guards return values, and the legacy next callback is easy to call twice.
  • Calling useAuthStore() at module top level before Pinia is installed; call it inside the guard function.
  • Forgetting the redirect query, which sends users to the home page after login instead of where they wanted to go.
Quick Quiz
Question 1 of 2

What does returning `false` from a `beforeEach` guard do?

Key Takeaways

  • children routes render inside the parent's <RouterView>, enabling shared layouts; props: true passes params as props.
  • Guards return true/undefined to allow, false to cancel, or a location to redirect; meta carries flags like requiresAuth.
  • onBeforeRouteLeave and onBeforeRouteUpdate handle unsaved changes and param-only changes.
  • component: () => import(...) splits each route into a lazily loaded chunk.
  • scrollBehavior, the catch-all route and RouterView slot transitions complete a production router.

Next lesson: State Management with Pinia — centralize shared state with stores, getters and actions.

Vue Router Advanced: Nested Routes, Navigation Guards and Lazy Loading - Vue.js | CodeYourCraft | CodeYourCraft