Vue 02 - Components & Routing
Components
Components are reusable building blocks. Each component is a .vue SFC file with its own template, logic, and styles.
Creating and using a component
ButtonCounter.vue
<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);
const increment = () => {
count.value++;
};
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
App.vue — import and use the component as a custom HTML tag:
<script setup lang="ts">
import ButtonCounter from "./components/ButtonCounter.vue";
</script>
<template>
<h1>My App</h1>
<ButtonCounter />
<ButtonCounter />
</template>
Each <ButtonCounter /> instance maintains its own independent state.
Props
Props pass data from parent to child. Use defineProps with a TypeScript interface for type safety.
<script setup lang="ts">
interface Props {
foo: string;
bar?: number;
}
const props = defineProps<Props>();
</script>
Default values
Use withDefaults to provide default values for optional props:
export interface Props {
msg?: string;
labels?: string[];
}
const props = withDefaults(defineProps<Props>(), {
msg: "hello",
labels: () => ["one", "two"],
});
Using props in parent
<script setup lang="ts">
import ButtonCounter from "./components/ButtonCounter.vue";
</script>
<template>
<!-- Static prop -->
<ButtonCounter msg="Hello" />
<!-- Dynamic prop (bound to a variable or expression) -->
<ButtonCounter :msg="greeting" :labels="['a', 'b']" />
</template>
Custom events (emits)
Child components communicate back to parents through events. Use defineEmits for type-safe event declarations.
<script setup lang="ts">
const emit = defineEmits<{
(e: "change", id: number): void;
(e: "update", value: string): void;
}>();
const handleClick = () => {
emit("change", 42);
};
</script>
<template>
<button @click="handleClick">Change</button>
<!-- Or emit directly in template -->
<button @click="$emit('update', 'new value')">Update</button>
</template>
Parent listens to the event:
<MyComponent @change="(id) => console.log(id)" @update="handleUpdate" />
Unlike native DOM events, component emitted events do not bubble.
Component v-model
v-model on a component creates two-way binding. Since Vue 3.4, use the defineModel macro for the simplest approach:
<script setup lang="ts">
const title = defineModel<string>("title");
</script>
<template>
<input type="text" v-model="title" />
</template>
Parent usage:
<MyComponent v-model:title="bookTitle" />
The older pattern (before 3.4) uses defineProps + defineEmits with update:propName:
<script setup lang="ts">
defineProps<{ title: string }>();
defineEmits<{ (e: "update:title", value: string): void }>();
</script>
<template>
<input
type="text"
:value="title"
@input="$emit('update:title', ($event.target as HTMLInputElement).value)"
/>
</template>
Slots
Slots let a parent inject content into a child component's template. The child defines where the content appears with <slot>.
<!-- Parent -->
<FancyButton>
Click me!
</FancyButton>
<!-- FancyButton.vue -->
<template>
<button class="fancy-btn">
<slot>
Submit
<!-- fallback content if nothing provided -->
</slot>
</button>
</template>
Named slots
For multiple content areas, use named slots:
<!-- Parent -->
<PageLayout>
<template #header>
<h1>Page Title</h1>
</template>
<template #default>
<p>Main content here</p>
</template>
<template #footer>
<p>Footer content</p>
</template>
</PageLayout>
<!-- PageLayout.vue -->
<template>
<header><slot name="header"></slot></header>
<main><slot></slot></main>
<footer><slot name="footer"></slot></footer>
</template>
Slot content has access to the parent's scope, not the child's.
Template refs
For direct access to DOM elements, use template refs.
<script setup lang="ts">
import { ref, onMounted } from "vue";
const inputRef = ref<HTMLInputElement | null>(null);
onMounted(() => {
inputRef.value?.focus();
});
</script>
<template>
<input ref="inputRef" />
</template>
Vue Lifecycle

Key lifecycle hooks in Composition API:
import {
onMounted,
onUpdated,
onUnmounted,
onBeforeMount,
onBeforeUpdate,
onBeforeUnmount,
onErrorCaptured,
onRenderTracked,
onRenderTriggered,
onActivated,
onDeactivated,
onServerPrefetch,
} from "vue";
onMounted(() => {
// Component is mounted to DOM — access DOM elements, start timers, fetch data
});
onUnmounted(() => {
// Cleanup — remove event listeners, clear timers
});
Most commonly used: onMounted (initial data fetching, DOM access) and onUnmounted (cleanup).
Vue Router
Vue Router provides client-side navigation between views without full page reloads.
Install (or select during project creation):
npm install vue-router@4
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>