The Complete Vue.js Guide
Master Vue.js 3 — the progressive JavaScript framework for building reactive, component-based user interfaces with elegance and simplicity.
Why Vue.js?
Vue.js was created by Evan You in 2014 and has grown into one of the most popular frontend frameworks. It was designed to be incrementally adoptable — you can use it for a small part of a page or build a full SPA. Vue 3 introduced the Composition API, making logic reuse clean and TypeScript integration seamless.
Vue's gentle learning curve, excellent documentation, and flexible architecture make it a favorite among developers who want productivity without sacrificing power. Its reactivity system automatically tracks dependencies and efficiently updates the DOM.
Vue powers apps at Alibaba, Xiaomi, GitLab, and thousands of companies worldwide.
Quick start: Run npm create vue@latest to scaffold a new Vue 3 project with Vite.
1. Vue Basics & Setup
Your First Vue Component (Composition API)
Vue 3 Single File Components (SFCs) combine template, script, and styles in one .vue file. The Composition API with script setup is the modern way to write Vue.
<!-- App.vue -->
<template>
<div class="app">
<h1>{{ title }}</h1>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
<button @click="reset">Reset</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const title = 'My Vue App'
const count = ref(0)
function increment() {
count.value++
}
function reset() {
count.value = 0
}
</script>
<style scoped>
.app {
max-width: 600px;
margin: 0 auto;
padding: 2rem;
}
button {
margin: 0.5rem;
padding: 0.5rem 1rem;
}
</style>Computed Properties and Watchers
Computed properties are reactive values derived from other reactive data. Watchers let you run side effects when reactive data changes.
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// Computed property — auto-updates when deps change
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
const price = ref(100)
const quantity = ref(2)
const total = computed(() => price.value * quantity.value)
// Watcher — run side effect on change
watch(total, (newVal, oldVal) => {
console.log(`Total changed from ${oldVal} to ${newVal}`)
})
// watchEffect — auto-tracks dependencies
import { watchEffect } from 'vue'
watchEffect(() => {
console.log('Full name is now:', fullName.value)
})
</script>2. Reactivity & Composition API
ref, reactive, and Composables
ref() wraps primitives in a reactive container. reactive() makes objects deeply reactive. Composables are functions that encapsulate reusable reactive logic.
// useCounter.ts — Composable
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubled = computed(() => count.value * 2)
function increment() { count.value++ }
function decrement() { count.value-- }
function reset() { count.value = initialValue }
return { count, doubled, increment, decrement, reset }
}
// useFetch.ts — Composable for data fetching
import { ref, onMounted } from 'vue'
export function useFetch<T>(url: string) {
const data = ref<T | null>(null)
const loading = ref(false)
const error = ref<string | null>(null)
async function fetchData() {
loading.value = true
try {
const res = await fetch(url)
data.value = await res.json()
} catch (e) {
error.value = 'Fetch failed'
} finally {
loading.value = false
}
}
onMounted(fetchData)
return { data, loading, error, refetch: fetchData }
}3. Components & Props
Props, Emits, and Slots
Components communicate through props (parent to child) and emits (child to parent). Slots allow parent components to inject content into child components.
<!-- Button.vue — Child component -->
<template>
<button
:class="['btn', variant]"
:disabled="disabled"
@click="handleClick"
>
<slot>Click me</slot>
</button>
</template>
<script setup lang="ts">
interface Props {
variant?: 'primary' | 'secondary' | 'danger'
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
variant: 'primary',
disabled: false,
})
const emit = defineEmits<{
click: [event: MouseEvent]
}>()
function handleClick(event: MouseEvent) {
if (!props.disabled) {
emit('click', event)
}
}
</script>
<!-- Parent usage -->
<template>
<Button variant="primary" @click="handleSave">
Save Changes
</Button>
<Button variant="danger" :disabled="loading" @click="handleDelete">
Delete Item
</Button>
</template>4. Directives & Template Syntax
Core Vue Directives
Vue's template directives — v-if, v-for, v-model, v-bind, v-on — provide declarative control over the DOM.
<template>
<!-- Conditional rendering -->
<div v-if="isLoggedIn">Welcome back, {{ username }}!</div>
<div v-else-if="isGuest">Hello, Guest!</div>
<div v-else>Please sign in</div>
<!-- v-show (keeps in DOM, toggles visibility) -->
<div v-show="showPanel">Side panel content</div>
<!-- List rendering with v-for -->
<ul>
<li
v-for="(item, index) in items"
:key="item.id"
:class="{ active: item.selected }"
>
{{ index + 1 }}. {{ item.name }}
</li>
</ul>
<!-- Two-way binding with v-model -->
<input v-model="searchQuery" placeholder="Search..." />
<select v-model="selectedCategory">
<option v-for="cat in categories" :key="cat" :value="cat">
{{ cat }}
</option>
</select>
<input type="checkbox" v-model="isActive" />
<p>Results for: {{ searchQuery }} | Active: {{ isActive }}</p>
</template>5. Vue Router
Setting Up Vue Router
Vue Router is the official router for Vue. It integrates deeply with Vue's reactivity system, supporting nested routes, navigation guards, and lazy loading.
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '@/views/HomeView.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', name: 'home', component: HomeView },
{
path: '/about',
name: 'about',
// Lazy loading
component: () => import('@/views/AboutView.vue'),
},
{
path: '/users/:id',
name: 'user',
component: () => import('@/views/UserView.vue'),
props: true, // pass route params as props
},
{
path: '/dashboard',
component: () => import('@/views/DashboardLayout.vue'),
children: [
{ path: '', component: () => import('@/views/DashboardHome.vue') },
{ path: 'settings', component: () => import('@/views/Settings.vue') },
],
},
],
})
// Navigation guard
router.beforeEach((to, from, next) => {
const isAuthenticated = !!localStorage.getItem('token')
if (to.meta.requiresAuth && !isAuthenticated) {
next({ name: 'home' })
} else {
next()
}
})
export default router6. State Management with Pinia
Creating and Using a Pinia Store
Pinia is the official state management library for Vue 3. It's type-safe, devtools-friendly, and much simpler than Vuex.
// stores/useAuthStore.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useAuthStore = defineStore('auth', () => {
const user = ref<{ name: string; email: string } | null>(null)
const token = ref<string | null>(localStorage.getItem('token'))
const isAuthenticated = computed(() => !!token.value)
const userName = computed(() => user.value?.name ?? 'Guest')
async function login(email: string, password: string) {
// API call
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
})
const data = await response.json()
token.value = data.token
user.value = data.user
localStorage.setItem('token', data.token)
}
function logout() {
user.value = null
token.value = null
localStorage.removeItem('token')
}
return { user, token, isAuthenticated, userName, login, logout }
})
// Usage in component
// const auth = useAuthStore()
// auth.login('user@example.com', 'password')
// console.log(auth.isAuthenticated)Keep Building with Vue!
Vue.js empowers you to build fast, interactive UIs with clean, readable code. Its ecosystem — Vite, Pinia, Vue Router, VueUse — makes it a complete framework.