vuejsbeginner
Vue.js Fundamentals — Components, Reactivity & Composition API
Build your first Vue 3 app — components, reactive state with the Composition API, directives, props, events, routing, and Pinia state management.
LearnixoApril 16, 20266 min read
Vue.jsVue 3Composition APIPiniaBeginner
Vue 3 is the approachable, performant, and versatile JavaScript framework. Simpler than Angular, more opinionated than React's ecosystem. With the Composition API, it scales from a simple widget to a full production SPA.
Setup
Bash
npm create vue@latest my-app
# Select: TypeScript, Router, Pinia, ESLint
cd my-app
npm install
npm run devYour First Component
src/components/UserCard.vue
VUE
<script setup lang="ts">
// Composition API — everything in <script setup>
import { ref, computed } from 'vue'
// Props (reactive, typed)
const props = defineProps<{
name: string
email: string
role?: string
}>()
// Reactive state
const isExpanded = ref(false)
// Computed property
const displayRole = computed(() =>
props.role ? props.role.toUpperCase() : 'USER'
)
// Emits
const emit = defineEmits<{
select: [userId: string]
}>()
</script>
<template>
<div class="card" @click="isExpanded = !isExpanded">
<h3>{{ name }}</h3>
<p>{{ email }}</p>
<span class="badge">{{ displayRole }}</span>
<div v-if="isExpanded" class="details">
<slot /> <!-- content projection (like React children) -->
</div>
</div>
</template>
<style scoped>
.card { border: 1px solid #e2e8f0; padding: 1rem; border-radius: 8px; }
.badge { background: #7c3aed; color: white; padding: 2px 8px; border-radius: 4px; }
</style>Reactivity — ref and reactive
TYPESCRIPT
import { ref, reactive, computed, watch } from 'vue'
// ref — for primitives (and objects when you want .value)
const count = ref(0)
const name = ref('Alice')
const isOpen = ref(false)
count.value++ // access with .value in script
// In template: {{ count }} ← no .value needed
// reactive — for objects (deep reactivity)
const user = reactive({
name: 'Alice',
email: 'alice@example.com',
role: 'admin',
})
user.name = 'Alice Smith' // direct mutation — reactive tracks it
// computed — derived state, memoised
const fullName = computed(() => `${user.firstName} ${user.lastName}`)
// watch — side effects when state changes
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`)
})
// watchEffect — runs immediately and re-runs when dependencies change
watchEffect(() => {
console.log('Name changed:', name.value)
})Template Directives
VUE
<template>
<!-- v-if / v-else-if / v-else — conditional rendering (removes from DOM) -->
<p v-if="isLoggedIn">Welcome back!</p>
<p v-else>Please log in</p>
<!-- v-show — hides (display:none), stays in DOM -->
<div v-show="isVisible">This is hidden when false</div>
<!-- v-for — list rendering -->
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
<!-- v-model — two-way binding on inputs -->
<input v-model="searchTerm" type="text" placeholder="Search..." />
<!-- v-bind (:) — bind attribute to expression -->
<img :src="imageUrl" :alt="altText" />
<button :disabled="isLoading">Submit</button>
<!-- v-on (@) — event listener -->
<button @click="handleClick">Click me</button>
<form @submit.prevent="handleSubmit">...</form>
<!-- Dynamic class/style -->
<div :class="{ active: isActive, 'text-red': hasError }">...</div>
<div :style="{ color: textColor, fontSize: fontSize + 'px' }">...</div>
</template>Props & Emits
VUE
<!-- Parent -->
<template>
<ProductCard
:product="selectedProduct"
:show-price="true"
@add-to-cart="handleAddToCart"
@remove="handleRemove"
/>
</template>VUE
<!-- Child (ProductCard.vue) -->
<script setup lang="ts">
interface Product { id: number; name: string; price: number }
const props = defineProps<{
product: Product
showPrice?: boolean
}>()
// Default values
withDefaults(defineProps<{ showPrice?: boolean }>(), {
showPrice: true,
})
const emit = defineEmits<{
addToCart: [product: Product]
remove: [productId: number]
}>()
function addToCart() {
emit('addToCart', props.product)
}
</script>Composables — Reusable Logic
Composables are the Vue equivalent of React hooks — functions that use Vue's reactivity APIs:
TYPESCRIPT
// src/composables/useFetch.ts
import { ref, watchEffect } from 'vue'
export function useFetch<T>(url: string) {
const data = ref<T | null>(null)
const isLoading = ref(false)
const error = ref<string | null>(null)
watchEffect(async () => {
isLoading.value = true
error.value = null
try {
const res = await fetch(url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = await res.json()
} catch (e) {
error.value = (e as Error).message
} finally {
isLoading.value = false
}
})
return { data, isLoading, error }
}Usage in a component:
VUE
<script setup>
import { useFetch } from '@/composables/useFetch'
const { data: products, isLoading, error } = useFetch<Product[]>('/api/products')
</script>
<template>
<div v-if="isLoading">Loading...</div>
<div v-else-if="error">{{ error }}</div>
<ul v-else>
<li v-for="p in products" :key="p.id">{{ p.name }}</li>
</ul>
</template>Vue Router (Basics)
TYPESCRIPT
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: () => import('@/views/HomeView.vue') },
{ path: '/products', component: () => import('@/views/ProductsView.vue') },
{ path: '/products/:id', component: () => import('@/views/ProductDetailView.vue') },
{ path: '/:pathMatch(.*)*', component: () => import('@/views/NotFoundView.vue') },
],
})
// Navigation guard
router.beforeEach((to, from) => {
const isAuth = useAuthStore().isLoggedIn
if (to.meta.requiresAuth && !isAuth) {
return { path: '/login', query: { redirect: to.fullPath } }
}
})
export default routerVUE
<!-- In templates -->
<RouterLink to="/products">Products</RouterLink>
<RouterLink :to="{ path: '/products', query: { category: 'electronics' } }">
Electronics
</RouterLink>TYPESCRIPT
// In script
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
const productId = route.params.id // path param
const page = route.query.page // query string
router.push('/products')
router.push({ path: '/products', query: { page: 2 } })Pinia State Management
TYPESCRIPT
// src/stores/cart.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCartStore = defineStore('cart', () => {
// State
const items = ref<CartItem[]>([])
// Getters (computed)
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
const count = computed(() =>
items.value.reduce((sum, item) => sum + item.quantity, 0)
)
// Actions
function add(product: Product, qty = 1) {
const existing = items.value.find(i => i.productId === product.id)
if (existing) {
existing.quantity += qty
} else {
items.value.push({ productId: product.id, name: product.name, price: product.price, quantity: qty })
}
}
function remove(productId: number) {
items.value = items.value.filter(i => i.productId !== productId)
}
function clear() {
items.value = []
}
return { items, total, count, add, remove, clear }
})Usage:
VUE
<script setup>
import { useCartStore } from '@/stores/cart'
const cart = useCartStore()
</script>
<template>
<p>Cart ({{ cart.count }}) — Total: {{ cart.total }}</p>
<button @click="cart.add(product)">Add to Cart</button>
</template>Quick Reference
Create app: npm create vue@latest
Component: