Nested Routes, Guards and Scroll Behavior
Vue Router is far more than a URL switcher. Anyone who applies nested routes, navigation guards and deliberate scroll behavior correctly builds single-page applications that feel like real web applications, with controlled access, clean loading behavior and not a single unexpected scroll position.
Table of Contents
- 1. What Vue Router does, and what it is not
- 2. Configuring the router instance and history modes correctly
- 3. Nested routes: layouts with nested router-view
- 4. Lazy loading: loading routes on demand
- 5. Navigation guards: beforeEach, beforeEnter and in-component
- 6. Route meta fields for auth and roles
- 7. Scroll behavior: control over the scroll position
- 8. Dynamic routes and params with props
- 9. Routing strategies compared
- 10. Summary
- 11. FAQ
1. What Vue Router does, and what it is not
Vue Router is the official routing library for Vue.js and takes on the task of mapping URL paths to components. In a single-page application, there is no more real page request: the browser loads the application once, and every further navigation event is intercepted by the router and handled on the client. This enables fast transitions without a reload, preserves application state and allows animated page transitions that would not be possible with classic multi-page apps.
What Vue Router is not: a state manager. Routing data such as the current params or query string values are short-lived navigation state, not persistent application data. Knowing the boundary between router state and store state is essential for a clean architecture. URL parameters that control the current view belong in the route. User data, API responses and UI state belong in Pinia or in composable state. Anyone who treats Vue Router as a simple link handler and ignores nested routes, guards and scroll behavior gives away a large part of what the library offers.
2. Configuring the router instance and history modes correctly
Vue Router 4 supports three history modes: createWebHistory for clean URLs without a hash (requires server configuration that redirects all paths to index.html), createWebHashHistory for hash-based URLs (no server setup required, but SEO drawbacks) and createMemoryHistory for server-side rendering. In production environments, createWebHistory is the right choice as long as the web server is configured correctly. On Nginx, a simple try_files $uri $uri/ /index.html; block in the location configuration is enough.
The router instance is created in its own file, src/router/index.ts, and registered in main.ts with app.use(router). Separating the router configuration from the app instance allows importing the router into composables and stores without risking circular dependencies. The scrollBehavior option is passed directly at creation time; it belongs on the router instance, not in individual route definitions.
// src/router/index.ts - Router instance with history mode and base URL
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const routes: RouteRecordRaw[] = [
{
path: '/',
component: () => import('@/layouts/DefaultLayout.vue'),
children: [
{
path: '',
name: 'home',
component: () => import('@/views/HomeView.vue'),
meta: { title: 'Startseite' }
},
{
path: 'blog',
name: 'blog',
component: () => import('@/views/BlogView.vue'),
meta: { title: 'Blog' }
}
]
},
{
path: '/admin',
component: () => import('@/layouts/AdminLayout.vue'),
meta: { requiresAuth: true, role: 'admin' },
children: [
{
path: '',
name: 'admin-dashboard',
component: () => import('@/views/admin/DashboardView.vue')
}
]
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
scrollBehavior(to, from, savedPosition) {
// Restore position on browser back/forward
if (savedPosition) return savedPosition
// Scroll to anchor if present
if (to.hash) return { el: to.hash, behavior: 'smooth' }
// Default: top of page
return { top: 0 }
}
})
export default router
3. Nested routes: layouts with nested router-view
Nested routes are the most powerful feature of Vue Router, and many projects do not use it to its full extent. The concept: a layout component contains a <router-view />, into which the child route renders. The layout itself, navigation, sidebar, footer, stays in place, and only the inner content changes. This avoids re-mounting components that stay identical across every route change, and it preserves the state of elements such as search fields or open dropdowns.
A typical pattern in complex applications is three layout levels: a global layout with top navigation, a section layout (for example, for the admin area with a sidebar) and the actual view component. Each level has its own <router-view />. The route configuration mirrors this hierarchy directly in the children array. Named views additionally make it possible to populate several <router-view /> instances at the same level, for example a main content area and a sidebar that shows different components depending on the route.
4. Lazy loading: loading routes on demand
Lazy loading in the context of Vue Router means that the JavaScript code of a view component is only loaded from the server once the corresponding route is visited for the first time. Without lazy loading, the code for every view ends up in the initial bundle, which noticeably slows down the load time of the first page, especially in larger applications. The syntax could not be simpler: instead of import HomeView from '@/views/HomeView.vue', you write component: () => import('@/views/HomeView.vue'). Vite and Webpack recognize this dynamic import pattern and automatically create separate chunks.
For better control over the generated chunks, using magic comments is recommended: () => import(/* webpackChunkName: "admin" */ '@/views/admin/DashboardView.vue'). All routes with the same chunk name end up in a shared bundle, which makes sense for module groups such as the admin area. In Vite projects, the Rollup equivalent /* @vite-ignore */ works for dynamic paths. A commonly overlooked optimization: preload critical routes in the background after the initial render with router.resolve({ name: 'checkout' }) combined with a dynamic import.
// src/router/index.ts - Lazy loading with chunk grouping
const routes: RouteRecordRaw[] = [
// Eager load: always in main bundle (critical path)
{
path: '/',
name: 'home',
component: HomeView
},
// Lazy load: separate chunk per route
{
path: '/produkte',
name: 'products',
component: () => import('@/views/ProductsView.vue')
},
// Grouped chunk: all admin views share one bundle
{
path: '/admin',
component: () => import(/* webpackChunkName: "admin" */ '@/layouts/AdminLayout.vue'),
children: [
{
path: 'users',
component: () => import(/* webpackChunkName: "admin" */ '@/views/admin/UsersView.vue')
},
{
path: 'orders',
component: () => import(/* webpackChunkName: "admin" */ '@/views/admin/OrdersView.vue')
}
]
}
]
// Preload checkout after initial render (background)
router.isReady().then(() => {
import('@/views/CheckoutView.vue')
})
5. Navigation guards: beforeEach, beforeEnter and in-component
Navigation guards are the mechanism through which Vue Router can intercept, check and redirect navigations. The global guard router.beforeEach runs on every navigation and is suited for cross-cutting logic such as authentication checks, page title updates and analytics tracking. The guard receives to (target route), from (source route) and returns either true (allow navigation), false (cancel), a route object (redirect) or nothing (allow, since undefined counts as true).
Route-level guards using beforeEnter on the route definition are ideal for route-specific checks that are not relevant to every route. In-component guards such as onBeforeRouteLeave (Composition API) make it possible to intercept leaving a component; a typical use case is unsaved form data. The guard can show a confirmation dialog and allow or cancel the navigation depending on the user's answer. Important: in-component guards do not run when the component is freshly mounted, only on route changes where the component is already active.
// src/router/guards.ts - Auth guard using Pinia store
import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
export async function authGuard(
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext
) {
const auth = useAuthStore()
// Initialize auth state from persisted token
if (!auth.initialized) {
await auth.initialize()
}
const requiresAuth = to.meta.requiresAuth as boolean | undefined
const requiredRole = to.meta.role as string | undefined
if (requiresAuth && !auth.isAuthenticated) {
// Preserve intended destination for post-login redirect
return next({ name: 'login', query: { redirect: to.fullPath } })
}
if (requiredRole && !auth.hasRole(requiredRole)) {
return next({ name: 'forbidden' })
}
// Update document title from route meta
if (to.meta.title) {
document.title = `${to.meta.title} | Mironsoft`
}
return next()
}
// In-component guard: warn on unsaved changes
// Usage in <script setup>
import { onBeforeRouteLeave } from 'vue-router'
onBeforeRouteLeave((to, from, next) => {
if (hasUnsavedChanges.value) {
const confirmed = window.confirm('Änderungen verwerfen?')
return next(confirmed)
}
next()
})
6. Route meta fields for auth and roles
Route meta fields are arbitrary data that you can attach to a route definition, and they are available in navigation guards and components via route.meta. The classic use case is declaring access rights directly in the route configuration: meta: { requiresAuth: true, role: 'admin' }. This declarative approach keeps access logic centralized in the router configuration and avoids duplicated checks in individual components. The global beforeEach guard then reads the meta fields and decides on the navigation.
Other useful meta fields: title for the page title (read out in the guard for document.title), layout for dynamic layout selection, transition for route-specific transition animations and keepAlive for controlling the <KeepAlive> component. Typing meta fields in TypeScript is done via module augmentation of the RouteMeta interface from vue-router. Without this typing, every meta field is of type unknown, which forces explicit casts in the guard.
7. Scroll behavior: control over the scroll position
The scroll behavior in Vue Router is the option that determines where the browser scrolls to after a navigation. Without explicit configuration, Vue Router inherits the default browser behavior, which is often wrong in SPAs: on back navigation the browser does not keep the scroll position, and on normal navigation it does not scroll to the top of the page. The scrollBehavior function receives the target route, the source route and the saved position (for browser back/forward).
For anchor navigation, the pattern if (to.hash) return { el: to.hash, behavior: 'smooth' } is the right approach. Vue Router finds the element with document.querySelector(to.hash) and scrolls there. In applications with server-side rendering or asynchronous components, the target DOM element may not yet be rendered at the moment of scrolling. In that case, a short delay with new Promise(resolve => setTimeout(resolve, 100)), or returning a promise from scrollBehavior that resolves once the element exists, is a good option.
8. Dynamic routes and params with props
Dynamic route segments such as /blog/:slug or /produkte/:categoryId/:productId are the tool for parameter-based views. The parameter is accessible in the component via route.params.slug. A more elegant and better testable approach is the props option: with props: true on the route definition, the params are passed to the component as props. The component then only needs to declare a prop slug: string and is fully decoupled from the router system; it also works without a router, which greatly simplifies unit tests.
For more complex transformations, props also supports functions: props: (route) => ({ id: parseInt(route.params.id as string) }) converts the URL parameter, which is always a string, into the correct TypeScript type. Optional chaining and fallback values in this function prevent undefined parameters from causing runtime errors. For routes with query string parameters, useRoute().query combined with a watcher that triggers API calls on parameter changes is a good fit, enabling filterable lists without a page reload.
9. Routing strategies compared
Choosing the right routing strategy significantly affects the maintainability, performance and security of a Vue Router configuration. The following table compares the most important approaches:
| Strategy | Approach | Recommendation | Benefit |
|---|---|---|---|
| Auth check | if(!auth) in every component |
Global beforeEach guard |
Centralized, no duplication |
| Using params | useRoute().params.id in component |
props: true on the route |
Component router-independent, testable |
| Code splitting | All imports eager | Dynamic import per route | Smaller initial bundle |
| Scroll | No scrollBehavior | scrollBehavior with savedPosition |
Correct back navigation |
| Layouts | Layout duplicated in every view | Nested routes with layout component | No re-mount on navigation |
Consistently using meta fields for permissions, props for parameters and nested routes for layouts are the three measures that bring the biggest quality improvement in a Vue Router configuration. Together they make the application more testable, the configuration more readable and the code more maintainable, without any significant extra implementation effort.
Mironsoft
Vue.js development, SPA architecture and frontend engineering
Need a Vue Router architecture for your project?
We analyze existing Vue applications, identify router anti-patterns and build a clean routing structure with guards, lazy loading and correct scroll behavior.
Router audit
Analysis of existing route configurations for security, performance and maintainability
Guard implementation
Role-based navigation guards with Pinia integration and TypeScript typing
Lazy loading
Code-splitting strategy for optimal initial load time and bundle sizes
10. Summary
Vue Router is only used to its full potential once nested routes, navigation guards, lazy loading and scroll behavior are applied deliberately. Nested routes enable layout hierarchies without component re-mounting. The global beforeEach guard with meta fields keeps access logic centralized. Dynamic imports reduce the initial bundle to what is truly necessary. The scrollBehavior function makes back navigation and anchor links behave correctly.
The decisive quality improvement comes from typing meta fields in TypeScript, using props instead of directly calling useRoute() in components, and consistently grouping related routes into chunks. A Vue Router configuration that follows these patterns is not only safer and faster, it is also considerably easier to test, because components do not depend on the router.
Vue Router: Nested Routes, Guards and Scroll Behavior - The Essentials
Nested routes
Layout components with <router-view /> as children, no re-mounting of navigation and footer elements on route changes.
Navigation guards
Global beforeEach with meta fields for auth and roles, centralized, declarative, without duplication in components.
Lazy loading
Dynamic imports for every view except the critical home page, groupable via magic comment for related areas.
Scroll behavior
scrollBehavior with savedPosition for browser back, to.hash for anchors and { top: 0 } as the default, making SPAs navigable like classic websites.
11. FAQ: Vue Router - Nested Routes, Guards and Scroll Behavior
1What are nested routes in Vue Router?
<router-view /> as children, the layout stays, only the content changes, no re-mounting of navigation and footer.2beforeEach vs. beforeEnter, when to use which?
3How does lazy loading work in Vue Router?
component: () => import('@/views/MyView.vue'). Vite/Webpack create separate chunks that only load on the route's first visit.4How do I fix the scroll behavior?
scrollBehavior when creating the router: savedPosition for back/forward, { el: to.hash } for anchors, { top: 0 } as the default.5Passing route params as props?
props: true on the route definition, the component declares a prop of the same name and is decoupled from the router, making it easier to test.6Which history mode for production?
createWebHistory for clean URLs without a hash, with a server fallback to index.html. createWebHashHistory only when no server setup is possible.7TypeScript typing for meta fields?
declare module 'vue-router' { interface RouteMeta { requiresAuth?: boolean } }, making all meta fields typed and usable without casts.8Preventing users from leaving with unsaved data?
onBeforeRouteLeave from the Composition API, shows a confirmation dialog and calls next(true/false) depending on the user's answer.9Grouping route chunks in Vite?
build.rollupOptions.output.manualChunks in vite.config.ts or via magic comment /* webpackChunkName: 'admin' */ in the dynamic import.10Vue Router without a browser environment?
createMemoryHistory for SSR and tests without a browser, no URL access, purely memory-based. The default for Nuxt.js SSR and Vitest tests using the router.