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

Why Vue.js 3? Better performance than Vue 2, Composition API for better code organization, improved TypeScript support, multiple root elements support, and tree-shaking for smaller bundles.

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');
Key Concept: Every Vue application starts by creating a new application instance with the createApp function.

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">
Important: Always use :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
})
Reactivity System: Vue automatically tracks dependencies and updates the DOM when reactive data changes.

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">
Form Validation Tip: Use computed properties to validate form data and show error messages dynamically.

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');
  }
}
Lifecycle Tips: Use 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
  }
}
Composition API Benefits: Better logic reuse, improved TypeScript support, and better organization for complex components.

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>
Summary: This guide covers everything you need to know about Vue.js 3 - from basic concepts to advanced patterns. Practice each concept with small examples, build projects using these patterns, and join the Vue community!