What is Vue.js 3?
Vue.js is a progressive JavaScript framework for building user interfaces and single-page applications (SPAs).
Reactive Data Binding
Automatic updates when data changes
Component-Based
Reusable, composable components
Virtual DOM
Efficient rendering and updates
Easy Learning
Gentle learning curve
TypeScript Support
First-class TypeScript integration
Composition API
Better code organization
Installation & Setup
A. CDN Method (Quick Start)
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
B. Using Vite (Recommended for Projects)
npm create vue@latest my-project
cd my-project
npm install
npm run dev
C. Basic HTML Template
<!DOCTYPE html>
<html>
<head>
<title>Vue.js 3 App</title>
</head>
<body>
<div id="app">
{{ message }}
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
message: 'Hello Vue 3!'
}
}
}).mount('#app');
</script>
</body>
</html>
Basic Concepts
A. Vue Instance
const app = createApp({
data() {
return {
message: 'Hello World'
}
},
methods: {
greet() {
alert('Hello!');
}
}
});
app.mount('#app');
Template Syntax
A. Mustache Syntax ({{ }})
<span>{{ message }}</span>
<span>{{ number + 1 }}</span>
<span>{{ ok ? 'YES' : 'NO' }}</span>
B. Attribute Binding
<!-- Long form -->
<img v-bind:src="imageSrc">
<button v-bind:disabled="isButtonDisabled">
<!-- Shorthand -->
<img :src="imageSrc">
<button :disabled="isButtonDisabled">
C. Event Binding
<!-- Long form -->
<button v-on:click="handleClick">Click me</button>
<!-- Shorthand -->
<button @click="handleClick">Click me</button>
Directives (A to Z)
v-bind
Purpose: Dynamically bind attributes to data
<img :src="imageUrl" :alt="imageAlt">
<div :class="{ active: isActive }">
<button :disabled="isDisabled">Submit</button>
v-model
Purpose: Create two-way data binding on form inputs
<input v-model="message" placeholder="Type something">
<textarea v-model="text"></textarea>
<input type="checkbox" v-model="checked">
<select v-model="selected">
<option value="A">Option A</option>
<option value="B">Option B</option>
</select>
v-if, v-else-if, v-else
Purpose: Conditionally render elements
<div v-if="type === 'A'">
Type A
</div>
<div v-else-if="type === 'B'">
Type B
</div>
<div v-else>
Not A or B
</div>
v-show
Purpose: Toggle element visibility with CSS
<div v-show="isVisible">This will be hidden with display: none</div>
v-for
Purpose: Render lists of data
<!-- Array -->
<li v-for="(item, index) in items" :key="item.id">
{{ index }} - {{ item.name }}
</li>
<!-- Object -->
<li v-for="(value, key) in object" :key="key">
{{ key }}: {{ value }}
</li>
<!-- Numbers -->
<span v-for="n in 10" :key="n">{{ n }}</span>
v-on
Purpose: Listen to DOM events
<button @click="handleClick">Click</button>
<button @click="count++">Increment</button>
<button @click="handleClick($event)">With Event</button>
<!-- Event Modifiers -->
<form @submit.prevent="onSubmit">
<button @click.stop="handleClick">
<input @keyup.enter="handleEnter">
:key
with v-for
for optimal performance and to avoid rendering issues.
Data & Reactivity
A. Data Function (Options API)
data() {
return {
message: 'Hello',
count: 0,
user: {
name: 'John',
age: 30
},
items: ['apple', 'banana', 'cherry']
}
}
B. Reactive Data (Composition API)
import { ref, reactive } from 'vue'
// Composition API
const count = ref(0)
const state = reactive({
name: 'John',
age: 30
})
Methods
Purpose: Define functions that can be called from templates or other methods
methods: {
// Simple method
greet() {
alert('Hello!');
},
// Method with parameters
greetUser(name) {
alert(`Hello, ${name}!`);
},
// Method that updates data
increment() {
this.count++;
},
// Async method
async fetchData() {
const response = await fetch('/api/data');
this.data = await response.json();
}
}
Usage in Template
<button @click="greet">Say Hello</button>
<button @click="greetUser('Vue')">Greet Vue</button>
<button @click="increment">Count: {{ count }}</button>
Computed Properties
Purpose: Create derived state that automatically updates when dependencies change
computed: {
// Simple computed property
fullName() {
return `${this.firstName} ${this.lastName}`;
},
// Computed property with getter and setter
fullNameWithSetter: {
get() {
return `${this.firstName} ${this.lastName}`;
},
set(value) {
const names = value.split(' ');
this.firstName = names[0];
this.lastName = names[names.length - 1];
}
},
// Complex computed property
expensiveValue() {
// This will only re-run when dependencies change
return this.items.filter(item => item.price > 100)
.reduce((sum, item) => sum + item.price, 0);
}
}
Computed Properties | Methods |
---|---|
Cached based on dependencies | Always executes when called |
Only re-evaluates when dependencies change | Re-evaluates on every render |
Better for expensive operations | Better for actions and side effects |
Watchers
Purpose: Perform side effects in response to data changes
watch: {
// Simple watcher
message(newValue, oldValue) {
console.log(`Message changed from ${oldValue} to ${newValue}`);
},
// Deep watcher for objects
user: {
handler(newValue, oldValue) {
console.log('User object changed');
},
deep: true
},
// Immediate watcher
count: {
handler(newValue) {
console.log(`Count is now ${newValue}`);
},
immediate: true
}
}
Composition API Watchers
import { watch, watchEffect } from 'vue'
// Watch a single ref
watch(count, (newCount, oldCount) => {
console.log(`Count: ${oldCount} -> ${newCount}`);
});
// Watch multiple sources
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
console.log('Count or name changed');
});
// Watch effect (automatically tracks dependencies)
watchEffect(() => {
console.log(`Count is ${count.value}`);
});
Event Handling
A. Basic Events
<button @click="handleClick">Click</button>
<input @input="handleInput" @focus="handleFocus">
<form @submit="handleSubmit">
B. Event Modifiers
<!-- Prevent default behavior -->
<form @submit.prevent="handleSubmit">
<!-- Stop event propagation -->
<button @click.stop="handleClick">
<!-- Key modifiers -->
<input @keyup.enter="handleEnter">
<input @keyup.esc="handleEscape">
<input @keyup.ctrl.a="handleCtrlA">
<!-- Mouse modifiers -->
<button @click.left="handleLeftClick">
<button @click.right="handleRightClick">
<button @click.middle="handleMiddleClick">
<!-- System modifiers -->
<button @click.ctrl="handleCtrlClick">
<button @click.shift="handleShiftClick">
Form Handling
A. Form Input Binding
<!-- Text Input -->
<input v-model="message" type="text">
<!-- Textarea -->
<textarea v-model="message"></textarea>
<!-- Checkbox -->
<input v-model="checked" type="checkbox">
<!-- Radio -->
<input v-model="picked" type="radio" value="A">
<input v-model="picked" type="radio" value="B">
<!-- Select -->
<select v-model="selected">
<option value="A">Option A</option>
<option value="B">Option B</option>
</select>
B. Form Modifiers
<!-- Lazy update (on change, not input) -->
<input v-model.lazy="message">
<!-- Convert to number -->
<input v-model.number="age" type="number">
<!-- Trim whitespace -->
<input v-model.trim="message">
Components
A. Component Registration
// Global Registration
app.component('my-component', {
template: `<div>{{ message }}</div>`,
data() {
return {
message: 'Hello from component!'
}
}
});
// Local Registration
export default {
components: {
MyComponent: {
template: `<div>Local Component</div>`
}
}
}
B. Single File Components (.vue)
<template>
<div class="my-component">
<h2>{{ title }}</h2>
<button @click="increment">Count: {{ count }}</button>
</div>
</template>
<script>
export default {
name: 'MyComponent',
data() {
return {
title: 'My Component',
count: 0
}
},
methods: {
increment() {
this.count++;
}
}
}
</script>
<style scoped>
.my-component {
padding: 20px;
border: 1px solid #ccc;
}
</style>
Props & Custom Events
A. Props (Parent to Child)
// Child Component
export default {
props: {
// Simple prop
message: String,
// Prop with validation
count: {
type: Number,
default: 0,
required: true,
validator(value) {
return value >= 0;
}
},
// Multiple types
id: [String, Number],
// Object prop
user: {
type: Object,
default() {
return { name: '', age: 0 };
}
}
}
}
Parent Template
<child-component
:message="parentMessage"
:count="parentCount"
:user="currentUser"
>
</child-component>
B. Custom Events (Child to Parent)
// Child Component
methods: {
handleClick() {
// Emit event to parent
this.$emit('item-clicked', this.item);
this.$emit('update:count', this.count + 1);
}
}
// Define emitted events (Vue 3)
emits: ['item-clicked', 'update:count']
Slots
A. Basic Slots
<!-- Child Component -->
<template>
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot> <!-- Default slot -->
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
<!-- Parent Usage -->
<child-component>
<template #header>
<h1>Page Title</h1>
</template>
<p>Main content goes here</p>
<template #footer>
<p>Footer content</p>
</template>
</child-component>
B. Scoped Slots
<!-- Child Component -->
<template>
<div>
<slot :user="user" :isActive="isActive"></slot>
</div>
</template>
<!-- Parent Usage -->
<child-component>
<template #default="{ user, isActive }">
<div :class="{ active: isActive }">
{{ user.name }}
</div>
</template>
</child-component>
Lifecycle Hooks
export default {
// Before component is created
beforeCreate() {
console.log('beforeCreate: Component instance is being created');
},
// Component is created
created() {
console.log('created: Component instance is created');
// Good place to fetch data
},
// Before component is mounted
beforeMount() {
console.log('beforeMount: Component is about to be mounted');
},
// Component is mounted
mounted() {
console.log('mounted: Component is mounted to DOM');
// DOM is available, good for DOM manipulation
},
// Before component is updated
beforeUpdate() {
console.log('beforeUpdate: Data changed, before DOM update');
},
// Component is updated
updated() {
console.log('updated: DOM has been updated');
},
// Before component is unmounted
beforeUnmount() {
console.log('beforeUnmount: Component is about to be unmounted');
// Cleanup timers, event listeners, etc.
},
// Component is unmounted
unmounted() {
console.log('unmounted: Component is unmounted');
}
}
created()
for data fetching, mounted()
for DOM manipulation, and beforeUnmount()
for cleanup.
Composition API
A. Basic Setup
import { ref, reactive, computed, onMounted } from 'vue'
export default {
setup() {
// Reactive data
const count = ref(0)
const state = reactive({
name: 'John',
age: 30
})
// Computed
const doubleCount = computed(() => count.value * 2)
// Methods
const increment = () => {
count.value++
}
// Lifecycle
onMounted(() => {
console.log('Component mounted')
})
// Return what template can use
return {
count,
state,
doubleCount,
increment
}
}
}
B. Composition Functions (Composables)
// useCounter.js
import { ref } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initialValue
return {
count,
increment,
decrement,
reset
}
}
Vue Router
Installation
npm install vue-router@4
Basic Setup
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
{ path: '/user/:id', component: User, props: true }
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
Router Usage
<template>
<div>
<!-- Navigation -->
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
<!-- Route component renders here -->
<router-view></router-view>
</div>
</template>
State Management (Pinia)
Installation
npm install pinia
Store Setup
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Vue'
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
},
async fetchUserData() {
const response = await fetch('/api/user')
this.userData = await response.json()
}
}
})
API Integration
Using Fetch
export default {
data() {
return {
users: [],
loading: false,
error: null
}
},
async mounted() {
await this.fetchUsers()
},
methods: {
async fetchUsers() {
try {
this.loading = true
const response = await fetch('/api/users')
if (!response.ok) {
throw new Error('Failed to fetch users')
}
this.users = await response.json()
} catch (error) {
this.error = error.message
} finally {
this.loading = false
}
}
}
}
Using Axios
import axios from 'axios'
// Create axios instance
const api = axios.create({
baseURL: '/api',
timeout: 10000,
})
// Request interceptor
api.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
Best Practices
Component Organization
- Use Single File Components (.vue)
- Keep components small and focused
- Use PascalCase for component names
- Use descriptive component names
Data Management
- Use computed properties for derived state
- Use methods for actions and event handlers
- Use watchers sparingly, prefer computed
- Keep data function pure
Performance
- Use v-show vs v-if appropriately
- Add :key to v-for items
- Use v-once for static content
- Lazy load components when needed
Security
- Never use v-html with user input
- Validate props properly
- Sanitize data from APIs
- Use CSP headers
Real-World Examples
Complete CRUD Component
<template>
<div class="user-manager">
<h2>User Management</h2>
<!-- Add User Form -->
<form @submit.prevent="addUser">
<input v-model="newUser.name" placeholder="Name" required>
<input v-model="newUser.email" placeholder="Email" required>
<button type="submit" :disabled="loading">Add User</button>
</form>
<!-- Users List -->
<div v-if="loading">Loading...</div>
<div v-else-if="error" class="error">{{ error }}</div>
<div v-else>
<div v-for="user in users" :key="user.id" class="user-item">
<span>{{ user.name }} ({{ user.email }})</span>
<button @click="editUser(user)">Edit</button>
<button @click="deleteUser(user.id)" class="danger">Delete</button>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'UserManager',
data() {
return {
users: [],
newUser: { name: '', email: '' },
loading: false,
error: null
}
},
async created() {
await this.fetchUsers()
},
methods: {
async fetchUsers() {
try {
this.loading = true
this.error = null
const response = await axios.get('/api/users')
this.users = response.data
} catch (error) {
this.error = 'Failed to fetch users'
} finally {
this.loading = false
}
},
async addUser() {
try {
await axios.post('/api/users', this.newUser)
this.newUser = { name: '', email: '' }
await this.fetchUsers()
} catch (error) {
this.error = 'Failed to add user'
}
},
async deleteUser(id) {
if (confirm('Are you sure?')) {
try {
await axios.delete(`/api/users/${id}`)
await this.fetchUsers()
} catch (error) {
this.error = 'Failed to delete user'
}
}
}
}
}
</script>
<style scoped>
.user-manager {
padding: 20px;
}
.user-item {
display: flex;
justify-content: space-between;
padding: 10px;
border: 1px solid #ccc;
margin: 5px 0;
}
.error {
color: red;
padding: 10px;
background: #ffeaa7;
border-radius: 4px;
}
.danger {
background: #d63031;
color: white;
}
</style>