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.
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:
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.
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:
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.
Inside <script setup>, two composables handle the cases where the component is already rendered:
<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.
Import route components with a dynamic import() so each becomes a separate chunk that downloads on first navigation:
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.
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>.
next() in guards; Vue Router 4 guards return values, and the legacy next callback is easy to call twice.useAuthStore() at module top level before Pinia is installed; call it inside the guard function.redirect query, which sends users to the home page after login instead of where they wanted to go.What does returning `false` from a `beforeEach` guard do?
children routes render inside the parent's <RouterView>, enabling shared layouts; props: true passes params as props.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.