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

Global beforeEach

src/router/index.ts:

Code explanation:

  • Return false to cancel navigation
  • Return route location object to redirect
  • Return true or undefined to proceed
  • to.meta holds 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:

typescript
import "vue-router";
 
declare module "vue-router" {
  interface RouteMeta {
    requiresAuth?: boolean;
    public?: boolean;
    title?: string;
  }
}

Per-Route beforeEnter

typescript
{
  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:

typescript
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:

vue
<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:

typescript
import HeavyView from "../views/HeavyView.vue"; // in main chunk

Lazy load splits chunks:

typescript
{
  path: "/reports",
  name: "reports",
  component: () => import("../views/ReportsView.vue"),
}

Vite names chunks automatically; optional magic comment:

typescript
component: () =>
  import(/* webpackChunkName: "reports" */ "../views/ReportsView.vue"),

Nested lazy children:

typescript
{
  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:

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:

typescript
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

  1. Add meta: { requiresAuth: true } to a settings route
  2. Lazy-load at least two route components
  3. Set document.title from meta.title in afterEach

FAQ

beforeEach vs beforeEnter?

Global vs single route—compose both.

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.