Skip to main content

Vue 03 - Router & State

Vue Router

Vue Router provides client-side navigation between views without full page reloads.

Install (or select during project creation):

npm install vue-router

Setup

Register the router in main.ts:

import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
import router from "./router";

const app = createApp(App);
app.use(createPinia());
app.use(router);
app.mount("#app");

This enables:

  • <RouterView> and <RouterLink> components
  • useRouter() and useRoute() composables

Route configuration

src/router/index.ts

import { createRouter, createWebHistory, type RouteRecordRaw } from "vue-router";
import HomeView from "../views/HomeView.vue";

const routes: RouteRecordRaw[] = [
{
path: "/",
name: "Home",
component: HomeView,
},
{
path: "/owners",
name: "OwnersIndex",
// Lazy loading — component is loaded only when route is visited
component: () => import("../views/owners/IndexView.vue"),
},
{
// Dynamic segment :id becomes a route param
path: "/owners/edit/:id",
name: "OwnersEdit",
component: () => import("../views/owners/EditView.vue"),
props: true, // Maps route params to component props
},
{
// Catchall — matches any unmatched path
path: "/:pathMatch(.*)*",
name: "NotFound",
component: () => import("../views/NotFoundView.vue"),
},
];

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});

export default router;

RouterView

The matched component renders where <RouterView /> is placed:

<template>
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/owners">Owners</RouterLink>
</nav>

<!-- Matched route component renders here -->
<RouterView />
</template>

Receiving route params as props

When props: true is set on the route, params are mapped to component props:

<script setup lang="ts">
const props = defineProps<{
id: string;
}>();

// props.id contains the value from the URL
</script>

Declarative — using <RouterLink>:

<RouterLink to="/owners">Owners</RouterLink>
<RouterLink :to="{ name: 'OwnersEdit', params: { id: owner.id } }">
Edit
</RouterLink>

Programmatic — using useRouter():

<script setup lang="ts">
import { useRouter, useRoute } from "vue-router";

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

const goToEdit = (id: string) => {
router.push({ name: "OwnersEdit", params: { id } });
};

// Access current route info
console.log(route.params.id);
console.log(route.query.search);
</script>

Guards protect routes — for example, redirecting unauthenticated users to login.

Global guard in src/router/index.ts:

import { useAuthStore } from "@/stores/auth";

router.beforeEach((to, from) => {
const authStore = useAuthStore();

// List of routes that require authentication
const protectedRoutes = ["OwnersIndex", "OwnersEdit"];

if (protectedRoutes.includes(to.name as string) && !authStore.isAuthenticated) {
return { name: "Login" };
}
});

Watching route changes

When the same component is reused for different route params (e.g., /owners/edit/1 then /owners/edit/2), use a watcher to react to param changes:

<script setup lang="ts">
import { useRoute } from "vue-router";
import { ref, watch } from "vue";

const route = useRoute();
const userData = ref();

watch(
() => route.params.id,
async (newId) => {
userData.value = await fetchUser(newId as string);
}
);
</script>

Pinia state management

As apps grow, passing data through long chains of props becomes unwieldy. State management provides a centralized place for data that is shared across components — user info in the navbar, shopping cart contents, auth tokens, etc.

Pinia is the official state management library for Vue. https://pinia.vuejs.org/

Setup

Register Pinia in main.ts:

import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
import router from "./router";

const app = createApp(App);
app.use(createPinia());
app.use(router);
app.mount("#app");

Defining a store

A store holds state and business logic. Use the Composition API style (Setup Store):

// src/stores/counter.ts
import { ref, computed } from "vue";
import { defineStore } from "pinia";

export const useCounterStore = defineStore("counter", () => {
// ref() → state
const count = ref(0);

// computed() → getters
const doubleCount = computed(() => count.value * 2);

// functions → actions
const increment = () => count.value++;
const incrementBy = (val: number) => (count.value += val);

return { count, doubleCount, increment, incrementBy };
});

Using a store in components

<script setup lang="ts">
import { useCounterStore } from "@/stores/counter";

const store = useCounterStore();
</script>

<template>
<div>Count: {{ store.count }} (double: {{ store.doubleCount }})</div>
<button @click="store.increment()">+1</button>
<button @click="store.incrementBy(5)">+5</button>
</template>

Destructuring store — storeToRefs

To destructure reactive state from a store, use storeToRefs(). Actions (functions) can be destructured directly.

import { storeToRefs } from "pinia";
import { useCounterStore } from "@/stores/counter";

const store = useCounterStore();
const { count, doubleCount } = storeToRefs(store);
const { increment, incrementBy } = store;

Defence Preparation

Be prepared to explain topics like these:

  1. How does client-side routing in a SPA differ from traditional server-side routing? — In server-side routing, every URL change triggers a full HTTP request to the server, which returns a new HTML page. In client-side routing, the JavaScript router intercepts URL changes, updates the browser's address bar using the History API, and swaps the rendered component — all without a page reload. This is faster and preserves application state, but requires server configuration (like try_files in nginx) to handle direct URL access.
  2. How do dynamic route parameters work in Vue Router? How does a component access them? — Dynamic segments are defined with a colon: { path: '/users/:id', component: UserView }. When the URL matches (e.g., /users/42), the router extracts 42 as the id parameter. The component accesses it with useRoute().params.id. With props: true on the route, the parameter is passed as a prop instead, which decouples the component from the router and makes it easier to test.
  3. What are navigation guards, and what is a common use case? — Navigation guards are hooks that run before a route change is finalized. router.beforeEach((to, from) => { ... }) runs globally before every navigation. If the guard returns false or a different route, navigation is canceled or redirected. The most common use case is authentication: checking if the user is logged in before allowing access to protected routes, and redirecting to a login page if not.
  4. Why use lazy loading for routes, and how does it work? — Lazy loading splits each route's component into a separate JavaScript chunk that is downloaded only when the user navigates to that route. Instead of import UserView from './UserView.vue', you write component: () => import('./UserView.vue'). This reduces the initial bundle size, making the first page load faster. The trade-off is a small delay when navigating to a new route for the first time while the chunk downloads.
  5. When should you use Pinia (global state) vs local component state? — Use local component state (ref(), reactive()) for data that only one component needs — form inputs, UI toggles, local counters. Use Pinia stores for state that is shared across multiple components or needs to survive component destruction — authentication status, shopping cart, user preferences, cached API data. Overusing global state makes components harder to understand and test; underusing it leads to prop drilling and duplicated data.
  6. Why do you need storeToRefs() when destructuring a Pinia store? — Pinia stores are reactive objects. If you destructure with const { count } = store, you get a plain value that loses reactivity — count will not update when the store changes. storeToRefs(store) wraps each state property and getter in a ref, preserving reactivity after destructuring. Actions (functions) do not need storeToRefs because they are plain functions that don't need to be reactive.
  7. How does Pinia's Setup Store pattern compare to the Options Store pattern? — The Setup Store uses the Composition API style inside defineStore: ref() for state, computed() for getters, and plain functions for actions. The Options Store uses an object with state, getters, and actions properties. The Setup Store is more flexible (you can use any composable), mirrors how you write component logic, and is the recommended approach. Both produce the same reactive store; the difference is purely syntactic.