Router Guards and Lazy Routes
Introduction
Navigation guards run before or after route changes—ideal for authentication, permission checks, and unsaved-change warnings. Lazy-loaded routes split code so users download only the JavaScript for pages they visit. This chapter covers beforeEach, per-route guards, component hooks, and import() with scrollBehavior.
Prerequisites
- Vue Router Basics
- Composables or Pinia for auth state
Global beforeEach
src/router/index.ts:
Code explanation:
- Return
falseto cancel navigation - Return route location object to redirect
- Return
trueorundefinedto proceed to.metaholds custom fields from route config
Replace localStorage checks with Pinia useAuthStore() in real apps—chapter 25 project.
Route meta Typing
src/router/meta.d.ts:
import "vue-router";
declare module "vue-router" {
interface RouteMeta {
requiresAuth?: boolean;
public?: boolean;
title?: string;
}
}Per-Route beforeEnter
{
path: "/admin",
component: () => import("../views/AdminView.vue"),
beforeEnter: (to, from) => {
const role = localStorage.getItem("role");
if (role !== "admin") return { name: "home" };
return true;
},
},Use when one route needs special logic without global guard noise.
afterEach
Document title and analytics:
router.afterEach((to) => {
document.title = (to.meta.title as string) ?? "Hello Vue App";
});Cannot block navigation—side effects only.
Component Guards
onBeforeRouteLeave — warn on unsaved edits:
<script setup lang="ts">
import { ref } from "vue";
import { onBeforeRouteLeave } from "vue-router";
const dirty = ref(false);
onBeforeRouteLeave(() => {
if (!dirty.value) return true;
return window.confirm("Discard unsaved changes?");
});
</script>onBeforeRouteUpdate — react to param change on same component:
Lazy Route Components
Static import bundles everything upfront:
import HeavyView from "../views/HeavyView.vue"; // in main chunkLazy load splits chunks:
{
path: "/reports",
name: "reports",
component: () => import("../views/ReportsView.vue"),
}Vite names chunks automatically; optional magic comment:
component: () =>
import(/* webpackChunkName: "reports" */ "../views/ReportsView.vue"),Nested lazy children:
{
path: "/settings",
component: () => import("../layouts/SettingsLayout.vue"),
children: [
{
path: "profile",
component: () => import("../views/settings/ProfileView.vue"),
},
],
},scrollBehavior
Restore scroll on back/forward; scroll top on new pages:
Auth Redirect After Login
LoginView.vue:
<script setup lang="ts">
import { useRouter, useRoute } from "vue-router";
const router = useRouter();
const route = useRoute();
async function login() {
localStorage.setItem("token", "demo-token");
const redirect = (route.query.redirect as string) ?? "/dashboard";
await router.push(redirect);
}
</script>Guard Async
beforeEach can return a Promise:
router.beforeEach(async (to) => {
if (!to.meta.requiresAuth) return true;
const auth = useAuthStore();
if (!auth.initialized) await auth.fetchSession();
return auth.isLoggedIn ? true : { name: "login" };
});Show loading UI at app root while auth initializes.
Practice Exercise
- Add
meta: { requiresAuth: true }to a settings route - Lazy-load at least two route components
- Set
document.titlefrommeta.titleinafterEach
FAQ
beforeEach vs beforeEnter?
Global vs single route—compose both.
Navigation duplicated error?
Vue Router 4 handles most cases; await router.push in async handlers.
Guard infinite redirect?
Ensure public routes like /login do not require auth in beforeEach.
Lazy route loading flash?
Use <Suspense> or route loading component—optional pattern.
Role-based access?
meta.roles array + check in beforeEach.
Hash mode guards?
Same guard API with createWebHashHistory.