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>componentsuseRouter()anduseRoute()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>
Navigation
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>
Navigation guards
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:
- 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_filesin nginx) to handle direct URL access. - 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 extracts42as theidparameter. The component accesses it withuseRoute().params.id. Withprops: trueon the route, the parameter is passed as a prop instead, which decouples the component from the router and makes it easier to test. - 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 returnsfalseor 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. - 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 writecomponent: () => 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. - 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. - Why do you need
storeToRefs()when destructuring a Pinia store? — Pinia stores are reactive objects. If you destructure withconst { count } = store, you get a plain value that loses reactivity —countwill 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 needstoreToRefsbecause they are plain functions that don't need to be reactive. - 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 withstate,getters, andactionsproperties. 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.