# Navigation Guards and Advanced Routing

Course: [Vue.js](https://www.devtutorials.ai/learn/vue) · Section: Routing and State
Source: https://www.devtutorials.ai/learn/vue/routing-state/navigation-guards-and-advanced-routing

> Global guards (beforeEach, beforeResolve, afterEach), per-route beforeEnter, in-component onBeforeRouteLeave and onBeforeRouteUpdate, typed route meta, an auth guard with a redirect back, fetching data before or after navigation, scroll behaviour, lazy-loaded routes and moving focus after navigation.

## Guarding Navigation

- Some pages must not open for everyone. The kitchen screen is for staff only.
- Some pages must not close too easily. A half-filled form should warn first.
- A **navigation guard** = a function the router calls before or after moving to a page.
- A guard can **allow**, **cancel** or **redirect** the move.

**Simple lesson:** guards decide whether a navigation may happen.

### A Real-World Example

- Meera's site has a staff-only page: `/kitchen`.
- A visitor who is not logged in must go to `/login` instead.
- After logging in, they must land back on `/kitchen`.

</template>
`,
    "views/HomePage.vue": `<template>
    <h2>Welcome</h2>
</template>
`,
    "views/KitchenPage.vue": `<template>
    <h2>Kitchen screen: 3 orders waiting</h2>
</template>
`,
    "views/LoginPage.vue": `<script setup lang="ts">

const route = useRoute();
const router = useRouter();

function logIn(): void {
    loggedIn.value = true;
    router.push((route.query.redirect as string) || "/");
}
</script>

<template>
    <h2>Login</h2>
    <button @click="logIn">Log in as staff</button>
</template>
`,
  }}
/>

- **What happened:** "Kitchen" sent you to `/login?redirect=/kitchen`.
- After logging in, the login page pushed you back to `/kitchen`.

### What You Will Learn

1. The global guards and their order.
2. What a guard can return.
3. Guards on one route.
4. Guards inside a component.
5. Route meta, typed in TypeScript.
6. Loading data before or after a page opens.
7. Scroll behaviour.
8. Lazy-loaded routes.
9. Moving focus after navigation.

## Global Guards

- `router.beforeEach((to, from) => ...)` runs before **every** navigation.
- `router.beforeResolve(...)` runs last, just before the move is confirmed.
- `router.afterEach((to, from) => ...)` runs after the move. It cannot cancel anything.
- A guard can return:
  - nothing or `true`: allow
  - `false`: cancel
  - a path or route object: redirect
- A guard can be `async`. The router waits for it.
- Older code uses a third argument, `next()`. Returning a value is the modern way. Never mix both.
- Do not guard the page you redirect to. If `/login` also needs a login, the router loops forever.

<figure className="vue-fig ngc-fig">
<svg viewBox="0 0 640 170" role="img" aria-labelledby="fig-vue-guards" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-vue-guards">Animation: a navigation passes through beforeEach, beforeEnter, in-component guards, beforeResolve, then the page changes and afterEach runs. A guard that returns false stops the dot</title>
  <g style={{ fontFamily: "var(--mono)", fontSize: 11, fontWeight: 700 }}>
    <rect x="10" y="50" width="100" height="40" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }} /><text x="60" y="74" textAnchor="middle" style={{ fill: "var(--ngc-blue)" }}>beforeEach</text>
    <rect x="135" y="50" width="100" height="40" rx="8" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)" }} /><text x="185" y="74" textAnchor="middle" style={{ fill: "var(--ngc-purple)" }}>beforeEnter</text>
    <rect x="260" y="50" width="110" height="40" rx="8" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)" }} /><text x="315" y="74" textAnchor="middle" style={{ fill: "var(--ngc-purple)" }}>in-component</text>
    <rect x="395" y="50" width="110" height="40" rx="8" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }} /><text x="450" y="74" textAnchor="middle" style={{ fill: "var(--ngc-blue)" }}>beforeResolve</text>
    <rect x="530" y="50" width="100" height="40" rx="8" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)" }} /><text x="580" y="74" textAnchor="middle" style={{ fill: "var(--ngc-green)" }}>afterEach</text>
  </g>
  <path d="M110 70 H135 M235 70 H260 M370 70 H395 M505 70 H530" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }} />
  <circle r="8" cy="110" style={{ fill: "var(--ngc-green)" }}><animate attributeName="cx" values="60;185;315;450;580;580" keyTimes="0;0.15;0.3;0.45;0.6;1" dur="8s" repeatCount="indefinite" /><animate attributeName="opacity" values="1;1;0" keyTimes="0;0.6;0.62" dur="8s" repeatCount="indefinite" /></circle>
  <text x="320" y="140" textAnchor="middle" opacity="0" style={{ fill: "var(--ngc-green)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.45;0.5;0.6;0.62" dur="8s" repeatCount="indefinite" />Every guard allowed it: the page changes</text>
  <circle r="8" cy="110" opacity="0" style={{ fill: "var(--ngc-red)" }}><animate attributeName="cx" values="60;60;185;185" keyTimes="0;0.62;0.75;1" dur="8s" repeatCount="indefinite" /><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.62;0.63;0.95;1" dur="8s" repeatCount="indefinite" /></circle>
  <text x="320" y="140" textAnchor="middle" opacity="0" style={{ fill: "var(--ngc-red)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}><animate attributeName="opacity" values="0;0;1;1;0" keyTimes="0;0.75;0.77;0.95;1" dur="8s" repeatCount="indefinite" />beforeEnter returned false: cancelled</text>
</svg>
<figcaption>Guards run in order. Any guard can stop or redirect the navigation.</figcaption>
</figure>

</template>
`,
    "PageA.vue": `<template>
    <h2>Page A</h2>
</template>
`,
    "PageB.vue": `<script setup lang="ts">

onBeforeRouteLeave(() => console.log("3. onBeforeRouteLeave (leaving B)"));
</script>

<template>
    <h2>Page B</h2>
</template>
`,
  }}
/>

- **What happened:** the Console shows the guards in order.
- "B (closed)" was cancelled by `beforeEnter`, and `afterEach` reported it.

**Simple lesson:** return nothing to allow, `false` to cancel, a route to redirect.

## In-Component Guards

- `onBeforeRouteLeave` runs when leaving this page. Use it to protect unsaved work.
- `onBeforeRouteUpdate` runs when the same page is reused with new params, such as `/dish/1` → `/dish/2`.
- Both come from `vue-router` and must be called in `<script setup>`.

</template>
`,
    "FeedbackPage.vue": `<script setup lang="ts">

const text = ref<string>("");
const warning = ref<string>("");

onBeforeRouteLeave(() => {
    if (text.value && !warning.value) {
      warning.value = "You have unsaved feedback. Click the link again to leave anyway.";
      return false;
    }
});
</script>

<template>
    <h2>Feedback</h2>
    <textarea v-model="text" placeholder="Type something, then click a dish"></textarea>
    <p>{{ warning }}</p>
</template>
`,
    "DishPage.vue": `<script setup lang="ts">

const route = useRoute();
const loads = ref<number>(1);

onBeforeRouteUpdate((to, from) => {
    loads.value++;
    console.log(\`Same page, new param: \${from.params.id} → \${to.params.id}\`);
});
</script>

<template>
    <h2>Dish {{ route.params.id }}</h2>
    <p>Data loaded {{ loads }} times</p>
</template>
`,
  }}
/>

Real apps often ask with `window.confirm("Leave without saving?")` in `onBeforeRouteLeave`. This lab shows a warning on the page instead, because pop-up dialogs block the preview.

## Route Meta and Auth

- `meta` = extra data on a route, such as `{ requiresAuth: true, title: "Menu" }`.
- Guards read it: `to.meta.requiresAuth`.
- Meta of parent routes is merged into children.
- In TypeScript, add your fields to the `RouteMeta` interface with `declare module "vue-router"`, as in the first box.
- Keep the page the user wanted in `query.redirect`, and send them back after login.

## Fetching Data and Navigation

<div className="tscroll">

| Way | How | User sees |
|---|---|---|
| Fetch after navigation | Load in the page (`onMounted`, or `watch` on params) | The new page at once, with a loading message |
| Fetch before navigation | Load in `beforeEnter` or `beforeResolve`, then allow | The old page until data is ready |

</div>

- Fetching **before** lets a guard redirect when the data does not exist.

</template>
`,
    "DishPage.vue": `<script setup lang="ts">
</script>

<template>
    <h2>{{ current?.name }}: ₹{{ current?.price }}</h2>
</template>
`,
    "NotFound.vue": `<template>
    <p>Pick a dish. Missing dishes bring you back here.</p>
</template>
`,
  }}
/>

`beforeEnter` runs only when you **enter** the route from another route. Going from `/dish/2` to `/dish/3` does not run it again. Use `onBeforeRouteUpdate`, or a `watch` on the params, for that case.

## Scroll, Lazy Loading and Focus

- **Scroll behaviour:** `scrollBehavior(to, from, saved)` in `createRouter` decides where to scroll. Return `saved` for Back, `{ el: to.hash }` for anchors, or `{ top: 0 }`.
- **Lazy loading:** `component: () => import("./views/Big.vue")` loads a page only when it is first visited. The first bundle stays small.
- **Focus:** after a page change, screen-reader users need to know. Move focus to the new page's heading in `afterEach`.

</template>
`,
    "views/HomePage.vue": `<script setup lang="ts">
console.log("HomePage loaded");
</script>

<template>
    <h2>Home</h2>
</template>
`,
    "views/ReviewsPage.vue": `<script setup lang="ts">
console.log("ReviewsPage file loaded only now");
</script>

<template>
    <h2>Reviews</h2>
    <p>"Crispy dosa!" ⭐⭐⭐⭐⭐</p>
</template>
`,
  }}
/>

In a real build, each lazy route becomes its own JavaScript file, loaded over the network on the first visit. In this lab, all files are already in the preview, so "loaded only now" shows when the code first runs, not a network download.

**Simple lesson:** lazy-load big pages, scroll sensibly, and move focus to the new heading.

## TL;DR

- **Navigation guard** = a function that can allow, cancel or redirect a navigation.
- **`beforeEach` / `beforeResolve` / `afterEach`** = global: first / last before confirm / after.
- **Return values** = nothing = allow, `false` = cancel, a route = redirect.
- **`beforeEnter`** = a guard on one route. Only on entering it.
- **`onBeforeRouteLeave` / `onBeforeRouteUpdate`** = in-component: leaving / same page, new params.
- **Route meta** = extra route data. Type it with `RouteMeta`.
- **Fetch before / after** = in a guard / in the page.
- **Lazy route** = `component: () => import("./Page.vue")`.
- **`scrollBehavior` and focus** = where to scroll / focus the new heading in `afterEach`.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Auth check in every page | Each page should check | One `beforeEach` with `meta.requiresAuth` | A forgotten page is left open |
| Redirecting to the same guarded page | The guard runs once | Redirect to a page that is allowed | An endless redirect loop |
| Expecting `beforeEnter` on param change | It runs on every visit | Use `onBeforeRouteUpdate` or `watch` | Old data on the new param |
| Cancelling in `afterEach` | It can still stop navigation | Cancel in a `before` guard | The page has already changed |
| Losing the wanted page on login | Send users home after login | Keep `query.redirect` and push it | Users must find the page again |
| Lazy-loading every tiny page | Lazy is always faster | Lazy-load big or rare pages | Many tiny downloads |
| No focus change after navigation | Screen readers notice by themselves | Focus the new page's heading | Screen-reader users miss the change |

</div>

## Check Yourself

## Exercises

## What to Remember

- Guards can allow, cancel (`false`) or redirect (a route).
- Put auth in one `beforeEach`, driven by route `meta`.
- `onBeforeRouteLeave` protects unsaved work. `onBeforeRouteUpdate` handles new params.
- Fetch before navigation to redirect on missing data, or after it to show a loader.
- Lazy-load big pages, and focus the new heading after navigation.

Next: **State Management with Pinia**
