Vue Router
Baseline: Vue Router 5.x. See ../../STACK_BASELINE.md.
The official router. Unlike React, where routing is a third-party choice, Vue has one answer — which is why interviewers assume you know it.
Setup
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(), // or createWebHashHistory() / createMemoryHistory()
routes: [
{ path: '/', component: Home },
{ path: '/users/:id', component: User, props: true },
{ path: '/:pathMatch(.*)*', component: NotFound },
],
});
app.use(router);
| History mode | Use when |
|---|---|
createWebHistory |
normal apps; needs a server rewrite so deep links return index.html |
createWebHashHistory |
static hosting you cannot configure |
createMemoryHistory |
SSR and tests, where there is no browser URL |
The missing server rewrite is the single most common deployment bug: the app works when you navigate in-app and 404s on refresh.
Route params and props
{ path: '/users/:id', component: User, props: true }
props: true passes params as props, so the component takes an id prop instead of reaching into useRoute(). That makes it testable in isolation and reusable outside routing — prefer it.
Params are always strings. /users/1 gives "1", not 1, and comparing it to a numeric id silently fails.
Nested routes and layouts
{
path: '/settings',
component: SettingsLayout,
children: [
{ path: '', component: Profile }, // /settings
{ path: 'billing', component: Billing } // /settings/billing
],
}
The parent renders <RouterView /> where children appear. Nested routes are how you express shared layout, and named views (<RouterView name="sidebar" />) handle the multi-slot case.
Programmatic navigation
const router = useRouter(); // the router instance — for navigating
const route = useRoute(); // the current route — reactive, for reading
router.push({ name: 'user', params: { id: 7 } });
Use named routes with params rather than string concatenation: a path change then updates one route definition instead of every call site.
useRoute() returns a reactive object. Destructuring it (const { params } = useRoute()) loses reactivity — the same trap as reactive in 03_ref_vs_reactive.md. Use toRefs or read route.params at use time.
Navigation guards
router.beforeEach((to, from) => {
if (to.meta.requiresAuth && !auth.isLoggedIn) {
return { name: 'login', query: { redirect: to.fullPath } };
}
// return true or nothing to allow
});
Return false to cancel, a location to redirect, or nothing to continue. The old next() callback style still works but is legacy and easy to get wrong — calling next twice, or forgetting it, hangs navigation permanently.
Resolution order, worth being able to recite:
beforeEach(global)beforeEnter(per route)beforeRouteEnter(in-component, before the instance exists — nothis, use the callback form)beforeResolve(global, after async components resolve)afterEach(global, cannot cancel — this is where analytics goes)
Guards are not security. They control what the client renders; the API must authorise every request independently.
Lazy loading
{ path: '/admin', component: () => import('./views/Admin.vue') }
A dynamic import makes the route its own chunk. This is the default for anything not on the critical path, and the reason a Vue app’s initial bundle stays small as routes multiply.
Scroll behaviour
scrollBehavior(to, from, saved) {
if (saved) return saved; // restore on back/forward
if (to.hash) return { el: to.hash };
return { top: 0 };
}
Without this, an SPA keeps the previous scroll position when you navigate, which feels broken to users. Restoring saved on back navigation is the part people forget.
Data loading
Three approaches, in increasing order of how much they solve:
- Fetch in the component (
onMountedorwatchon the route) — simplest; gives you a flash of empty layout, and re-fetching on param change is manual. - Fetch in a guard — no flash, but navigation blocks until data arrives, so a slow API looks like a frozen app.
- Data loaders (
defineLoader/defineColadaLoader) — the router’s own mechanism: declare the fetch alongside the route, get deduplication, caching and revalidation, with navigation blocking or not as you choose.defineColadaLoaderintegrates Pinia Colada. See 23_data_fetching.md.
In Nuxt this is handled by useAsyncData / useFetch instead — see 11_nuxt.md.
Interview angle
- “Why does my app 404 on refresh but work when I click links?” - HTML5 history mode with no server rewrite. In-app navigation never hits the server; a refresh does, and the server has no file at that path. Configure the server to serve
index.htmlfor unmatched routes. - “Where do you enforce authentication?” - a
beforeEachguard for the UX, and the API for the actual authorisation. Saying only “a route guard” invites the follow-up about anyone calling the API directly. - “How do you lazy-load routes and when would you not?” - dynamic imports per route; skip it for the landing route, where an extra round trip costs more than it saves.
- “Why did my component not re-fetch when the route param changed?” - navigating from
/users/1to/users/2reuses the component instance, soonMounteddoes not run again. Watchroute.params, or key the<RouterView>by the param to force a remount. - “What is the difference between
useRouteanduseRouter?” -useRouteis the reactive current location for reading;useRouteris the instance for navigating. Mixing them up is a common early mistake.