What You'll Learn
Introduction to Vue.js
Vue.js is a progressive JavaScript framework for building user interfaces. Unlike monolithic frameworks, Vue is designed to be incrementally adoptable. The core library focuses on the view layer only, making it easy to integrate with other libraries or existing projects.
Why Vue.js?
Easy to Learn: Vue has a gentle learning curve with excellent documentation. Used by Alibaba, Xiaomi, GitLab, and Nintendo! Perfect for both beginners and experts.
- Reactive Data Binding: Automatic UI updates when data changes
- Component System: Reusable, self-contained UI pieces
- Virtual DOM: Efficient rendering for better performance
- Single-File Components: Template, script, and style in one .vue file
| Feature | Vue 2 | Vue 3 |
|---|---|---|
| API Style | Options API | Options + Composition API |
| Reactivity | Object.defineProperty | Proxy-based |
| Performance | Good | Faster & Smaller bundle |
| TypeScript | Limited support | Full support |
| State Management | Vuex | Pinia (recommended) |
Installation & Setup
Method 1: Using create-vue (Recommended)
Method 2: Using CDN (Quick Start)
<!DOCTYPE html> <html> <head> <title>My Vue App</title> <!-- Import Vue 3 from CDN --> <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> </head> <body> <div id="app"> {{ message }} </div> <script> const { createApp } = Vue createApp({ data() { return { message: 'Hello Vue!' } } }).mount('#app') </script> </body> </html>
Project Structure
Vue Project Created!
Open http://localhost:5173 to see your Vue app running!
Vue Basics & Instance
Creating a Vue Application
import { createApp } from 'vue' import App from './App.vue' import router from './router' import { createPinia } from 'pinia' // Create Vue application const app = createApp(App) // Use plugins app.use(router) app.use(createPinia()) // Global configuration app.config.errorHandler = (err) => { console.error('Global error:', err) } // Mount to DOM app.mount('#app')
Single-File Component (SFC)
<!-- Template - HTML structure --> <template> <div class="app"> <h1>{{ title }}</h1> <p>Count: {{ count }}</p> <button @click="increment">Increment</button> </div> </template> <!-- Script - JavaScript logic --> <script> export default { // Component name name: 'App', // Reactive data data() { return { title: 'Hello Vue!', count: 0 } }, // Methods methods: { increment() { this.count++ } } } </script> <!-- Style - CSS styling --> <style scoped> .app { text-align: center; padding: 20px; } button { padding: 10px 20px; font-size: 16px; cursor: pointer; } </style>
Scoped Styles
The scoped attribute ensures styles only apply to the current component,
preventing CSS conflicts with other components.
Template Syntax
Vue uses an HTML-based template syntax that allows you to declaratively bind the rendered DOM to the underlying component's data.
Text Interpolation
<template> <!-- Basic interpolation --> <p>Message: {{ message }}</p> <!-- JavaScript expressions --> <p>{{ message.split('').reverse().join('') }}</p> <p>{{ count + 1 }}</p> <p>{{ isActive ? 'Yes' : 'No' }}</p> <!-- Raw HTML (use with caution!) --> <p v-html="rawHtml"></p> <!-- One-time interpolation --> <p v-once>This will never change: {{ message }}</p> </template> <script> export default { data() { return { message: 'Hello Vue!', count: 5, isActive: true, rawHtml: '<strong>Bold text</strong>' } } } </script>
Attribute Binding
<template> <!-- v-bind shorthand : --> <img :src="imageUrl" :alt="imageAlt"> <!-- Dynamic class --> <div :class="{ active: isActive, error: hasError }"></div> <div :class="[activeClass, errorClass]"></div> <!-- Dynamic style --> <div :style="{ color: textColor, fontSize: fontSize + 'px' }"> Styled text </div> <!-- Multiple attributes --> <button v-bind="buttonAttrs">Click me</button> <!-- Boolean attributes --> <button :disabled="isDisabled">Submit</button> </template> <script> export default { data() { return { imageUrl: '/logo.png', imageAlt: 'Logo', isActive: true, hasError: false, activeClass: 'active', errorClass: '', textColor: '#42b883', fontSize: 18, isDisabled: false, buttonAttrs: { id: 'submit-btn', type: 'submit', class: 'btn btn-primary' } } } } </script>
Directives
Directives are special attributes with the v- prefix that apply reactive
behavior to the DOM.
Conditional Rendering (v-if, v-show)
<template> <!-- v-if: Adds/removes from DOM --> <div v-if="type === 'A'">Type A</div> <div v-else-if="type === 'B'">Type B</div> <div v-else>Type C</div> <!-- v-show: Toggles display CSS --> <p v-show="isVisible">I am visible!</p> <!-- v-if on template (no wrapper element) --> <template v-if="showGroup"> <h2>Title</h2> <p>Content</p> </template> <button @click="toggle">Toggle</button> </template> <script> export default { data() { return { type: 'A', isVisible: true, showGroup: true } }, methods: { toggle() { this.isVisible = !this.isVisible } } } </script>
v-if vs v-show
v-if: True conditional rendering - elements are destroyed/created.
v-show: Always rendered, just toggles CSS display property.
Use v-show for frequent toggles, v-if for rare changes.
List Rendering (v-for)
<template> <!-- Loop through array --> <ul> <li v-for="item in items" :key="item.id"> {{ item.name }} </li> </ul> <!-- With index --> <ul> <li v-for="(item, index) in items" :key="item.id"> {{ index + 1 }}. {{ item.name }} </li> </ul> <!-- Loop through object --> <div v-for="(value, key) in user" :key="key"> {{ key }}: {{ value }} </div> <!-- Range --> <span v-for="n in 10" :key="n">{{ n }} </span> <!-- v-for with v-if (use template) --> <template v-for="item in items" :key="item.id"> <li v-if="item.isActive">{{ item.name }}</li> </template> </template> <script> export default { data() { return { items: [ { id: 1, name: 'Vue', isActive: true }, { id: 2, name: 'React', isActive: true }, { id: 3, name: 'Angular', isActive: false } ], user: { name: 'Om Pandey', role: 'Developer', location: 'Nepal' } } } } </script>
Always Use :key
The :key attribute is essential for Vue to track each element's identity.
Always use unique identifiers, not array indices, for optimal performance.
Event Handling (v-on / @)
<template> <!-- Click event --> <button @click="handleClick">Click me</button> <!-- Inline expression --> <button @click="count++">Count: {{ count }}</button> <!-- Pass arguments --> <button @click="greet('Hello', $event)">Greet</button> <!-- Event modifiers --> <form @submit.prevent="onSubmit"> <input @keyup.enter="search"> <button type="submit">Submit</button> </form> <!-- Mouse modifiers --> <div @click.right="onRightClick">Right-click me</div> <!-- Once modifier --> <button @click.once="doOnce">Only once</button> <!-- Stop propagation --> <div @click="outer"> <button @click.stop="inner">Click</button> </div> </template> <script> export default { data() { return { count: 0 } }, methods: { handleClick() { console.log('Clicked!') }, greet(message, event) { console.log(message, event.target) }, onSubmit() { console.log('Form submitted!') }, search() { console.log('Searching...') } } } </script>
Two-Way Binding (v-model)
<template> <!-- Text input --> <input v-model="message" placeholder="Enter message"> <p>Message: {{ message }}</p> <!-- Textarea --> <textarea v-model="bio"></textarea> <!-- Checkbox --> <input type="checkbox" v-model="checked"> <label>{{ checked ? 'Checked' : 'Not checked' }}</label> <!-- Multiple checkboxes --> <input type="checkbox" v-model="hobbies" value="coding"> Coding <input type="checkbox" v-model="hobbies" value="gaming"> Gaming <input type="checkbox" v-model="hobbies" value="music"> Music <p>Hobbies: {{ hobbies }}</p> <!-- Radio buttons --> <input type="radio" v-model="gender" value="male"> Male <input type="radio" v-model="gender" value="female"> Female <!-- Select dropdown --> <select v-model="selected"> <option disabled value="">Select one</option> <option>Vue</option> <option>React</option> <option>Angular</option> </select> <!-- Modifiers --> <input v-model.lazy="lazy"> <!-- Sync on change --> <input v-model.number="age"> <!-- Cast to number --> <input v-model.trim="text"> <!-- Trim whitespace --> </template> <script> export default { data() { return { message: '', bio: '', checked: false, hobbies: [], gender: '', selected: '', age: 0 } } } </script>
Reactivity & Data Binding
Vue's reactivity system automatically tracks JavaScript state changes and efficiently updates the DOM when changes occur.
<template> <div> <p>Count: {{ count }}</p> <p>User: {{ user.name }} - {{ user.email }}</p> <ul> <li v-for="item in items" :key="item">{{ item }}</li> </ul> <button @click="updateData">Update Data</button> </div> </template> <script> export default { data() { return { count: 0, user: { name: 'Om', email: '[email protected]' }, items: ['Vue', 'React'] } }, methods: { updateData() { // All these trigger reactive updates this.count++ // Update object property this.user.name = 'Om Pandey' // Add new property (Vue 3 handles this automatically) this.user.age = 25 // Array mutations this.items.push('Angular') // Replace array this.items = [...this.items, 'Svelte'] } } } </script>
Computed Properties & Watchers
Computed Properties
Computed properties are cached based on their reactive dependencies and only re-evaluate when dependencies change.
<template> <div> <input v-model="firstName" placeholder="First name"> <input v-model="lastName" placeholder="Last name"> <p>Full Name: {{ fullName }}</p> <p>Reversed: {{ reversedFullName }}</p> <!-- Writable computed --> <input v-model="fullName"> <!-- Filtering example --> <input v-model="searchQuery" placeholder="Search..."> <ul> <li v-for="item in filteredItems" :key="item.id"> {{ item.name }} </li> </ul> </div> </template> <script> export default { data() { return { firstName: 'Om', lastName: 'Pandey', searchQuery: '', items: [ { id: 1, name: 'Vue.js' }, { id: 2, name: 'React' }, { id: 3, name: 'Angular' }, { id: 4, name: 'Svelte' } ] } }, computed: { // Basic computed property fullName: { // Getter get() { return this.firstName + ' ' + this.lastName }, // Setter set(newValue) { const [first, last] = newValue.split(' ') this.firstName = first this.lastName = last || '' } }, reversedFullName() { return this.fullName.split('').reverse().join('') }, // Filtered list filteredItems() { return this.items.filter(item => item.name.toLowerCase().includes(this.searchQuery.toLowerCase()) ) } } } </script>
Watchers
Watchers allow you to perform side effects when reactive data changes.
<template> <div> <input v-model="searchQuery" placeholder="Search..."> <p v-if="loading">Loading...</p> <p>Results: {{ results }}</p> </div> </template> <script> export default { data() { return { searchQuery: '', loading: false, results: null, user: { name: 'Om', profile: { email: '[email protected]' } } } }, watch: { // Basic watcher searchQuery(newValue, oldValue) { console.log(`Query changed from ${oldValue} to ${newValue}`) this.fetchResults(newValue) }, // Immediate watcher (runs on mount) searchQuery: { handler(newValue) { this.fetchResults(newValue) }, immediate: true }, // Deep watcher (watch nested properties) user: { handler(newValue) { console.log('User changed:', newValue) }, deep: true }, // Watch specific nested property 'user.profile.email'(newEmail) { console.log('Email changed to:', newEmail) } }, methods: { async fetchResults(query) { if (!query) return this.loading = true // Simulate API call await new Promise(r => setTimeout(r, 500)) this.results = `Results for: ${query}` this.loading = false } } } </script>
Components
Components are reusable Vue instances with their own template, logic, and styling. They form the building blocks of Vue applications.
Creating Components
<template> <button :class="['btn', `btn-${variant}`, { 'btn-disabled': disabled }]" :disabled="disabled" @click="handleClick" > <slot>Button</slot> </button> </template> <script> export default { name: 'BaseButton', props: { variant: { type: String, default: 'primary', validator: (value) => ['primary', 'secondary', 'danger'].includes(value) }, disabled: { type: Boolean, default: false } }, emits: ['click'], methods: { handleClick(event) { if (!this.disabled) { this.$emit('click', event) } } } } </script> <style scoped> .btn { padding: 10px 20px; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; transition: all 0.2s; } .btn-primary { background: #42b883; color: white; } .btn-secondary { background: #35495e; color: white; } .btn-danger { background: #ef4444; color: white; } .btn-disabled { opacity: 0.5; cursor: not-allowed; } </style>
Using Components
<template> <div class="home"> <h1>Welcome</h1> <!-- Using the Button component --> <BaseButton @click="handleClick"> Click Me </BaseButton> <BaseButton variant="secondary"> Secondary </BaseButton> <BaseButton variant="danger" disabled> Disabled </BaseButton> <!-- Using Card component --> <Card v-for="item in items" :key="item.id" :title="item.title" :description="item.description" /> </div> </template> <script> import BaseButton from '@/components/Button.vue' import Card from '@/components/Card.vue' export default { name: 'HomeView', components: { BaseButton, Card }, data() { return { items: [ { id: 1, title: 'Vue.js', description: 'Progressive Framework' }, { id: 2, title: 'Vite', description: 'Next Gen Build Tool' } ] } }, methods: { handleClick() { console.log('Button clicked!') } } } </script>
Props & Events
Props (Parent → Child)
<template> <div class="user-card"> <img :src="avatar" :alt="name"> <h3>{{ name }}</h3> <p>{{ email }}</p> <span :class="['badge', isActive ? 'active' : 'inactive']"> {{ isActive ? 'Online' : 'Offline' }} </span> </div> </template> <script> export default { name: 'UserCard', props: { // Simple prop name: String, // Required prop with type email: { type: String, required: true }, // Prop with default value avatar: { type: String, default: '/default-avatar.png' }, // Boolean prop isActive: { type: Boolean, default: false }, // Array/Object default (must be factory function) skills: { type: Array, default: () => [] }, // Custom validator role: { type: String, validator: (value) => { return ['admin', 'user', 'guest'].includes(value) } } } } </script>
Events (Child → Parent)
<template> <div class="counter"> <button @click="decrement">-</button> <span>{{ count }}</span> <button @click="increment">+</button> </div> </template> <script> export default { name: 'Counter', props: { count: { type: Number, required: true } }, // Declare emitted events emits: ['update:count', 'change'], methods: { increment() { this.$emit('update:count', this.count + 1) this.$emit('change', { action: 'increment', value: this.count + 1 }) }, decrement() { this.$emit('update:count', this.count - 1) this.$emit('change', { action: 'decrement', value: this.count - 1 }) } } } </script>
<template> <div> <!-- v-model for two-way binding --> <Counter v-model:count="myCount" @change="handleChange" /> <p>Parent Count: {{ myCount }}</p> </div> </template> <script> import Counter from '@/components/Counter.vue' export default { components: { Counter }, data() { return { myCount: 0 } }, methods: { handleChange(payload) { console.log('Counter changed:', payload) } } } </script>
Slots
Slots allow parent components to pass template content into child components.
<template> <div class="card"> <!-- Named slot: header --> <header class="card-header"> <slot name="header">Default Header</slot> </header> <!-- Default slot --> <main class="card-body"> <slot>Default content</slot> </main> <!-- Named slot: footer --> <footer class="card-footer"> <slot name="footer"></slot> </footer> </div> </template>
<template> <Card> <!-- Header slot --> <template #header> <h2>My Custom Header</h2> </template> <!-- Default slot content --> <p>This goes into the default slot!</p> <p>Multiple elements are allowed.</p> <!-- Footer slot --> <template #footer> <button>Save</button> <button>Cancel</button> </template> </Card> </template>
Scoped Slots
<template> <ul> <li v-for="item in items" :key="item.id"> <!-- Pass data to parent via slot props --> <slot :item="item" :index="index"> {{ item.name }} </slot> </li> </ul> </template> <!-- Usage --> <List :items="users"> <template #default="{ item, index }"> <span>{{ index + 1 }}. {{ item.name }} - {{ item.email }}</span> </template> </List>
Composition API
The Composition API provides a set of additive, function-based APIs that allow flexible composition of component logic.
<script setup> import { ref, reactive, computed, watch, onMounted } from 'vue' // Reactive references const count = ref(0) const message = ref('Hello') // Reactive object const user = reactive({ name: 'Om', email: '[email protected]' }) // Computed property const doubleCount = computed(() => count.value * 2) const fullName = computed({ get() { return user.name }, set(newValue) { user.name = newValue } }) // Methods function increment() { count.value++ } const decrement = () => { count.value-- } // Watchers watch(count, (newValue, oldValue) => { console.log(`Count: ${oldValue} → ${newValue}`) }) watch( () => user.name, (newName) => { console.log('Name changed:', newName) } ) // Lifecycle hooks onMounted(() => { console.log('Component mounted!') }) </script> <template> <div> <p>Count: {{ count }}</p> <p>Double: {{ doubleCount }}</p> <button @click="increment">+</button> <button @click="decrement">-</button> <input v-model="user.name"> <p>{{ user.name }} - {{ user.email }}</p> </div> </template>
Composables (Reusable Logic)
import { ref } from 'vue' export function useFetch(url) { const data = ref(null) const error = ref(null) const loading = ref(false) const fetchData = async () => { loading.value = true error.value = null try { const response = await fetch(url) data.value = await response.json() } catch (err) { error.value = err.message } finally { loading.value = false } } return { data, error, loading, fetchData } } // Usage in component // <script setup> // import { useFetch } from '@/composables/useFetch' // const { data, loading, error, fetchData } = useFetch('/api/users') // onMounted(fetchData) // </script>
Vue Router
Router Configuration
import { createRouter, createWebHistory } from 'vue-router' // Import views import Home from '@/views/Home.vue' import About from '@/views/About.vue' // Define routes const routes = [ { path: '/', name: 'Home', component: Home }, { path: '/about', name: 'About', component: About }, // Dynamic route { path: '/user/:id', name: 'User', component: () => import('@/views/User.vue'), props: true }, // Nested routes { path: '/dashboard', component: () => import('@/views/Dashboard.vue'), children: [ { path: '', name: 'DashboardHome', component: () => import('@/views/DashboardHome.vue') }, { path: 'settings', name: 'Settings', component: () => import('@/views/Settings.vue') } ] }, // Protected route { path: '/admin', name: 'Admin', component: () => import('@/views/Admin.vue'), meta: { requiresAuth: true } }, // 404 catch-all { path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('@/views/NotFound.vue') } ] // Create router instance const router = createRouter({ history: createWebHistory(), routes, scrollBehavior(to, from, savedPosition) { if (savedPosition) { return savedPosition } return { top: 0 } } }) // Navigation guard router.beforeEach((to, from, next) => { const isAuthenticated = localStorage.getItem('token') if (to.meta.requiresAuth && !isAuthenticated) { next({ name: 'Home' }) } else { next() } }) export default router
Using Router in Components
<template> <div id="app"> <nav> <!-- Router links --> <RouterLink to="/">Home</RouterLink> <RouterLink :to="{ name: 'About' }">About</RouterLink> <RouterLink :to="{ name: 'User', params: { id: 1 } }">User 1</RouterLink> </nav> <!-- Route content renders here --> <RouterView /> </div> </template>
<script setup> import { useRouter, useRoute } from 'vue-router' const router = useRouter() const route = useRoute() // Access route params const userId = route.params.id const query = route.query // Programmatic navigation function goToHome() { router.push('/') } function goToUser(id) { router.push({ name: 'User', params: { id } }) } function goBack() { router.back() } function replaceRoute() { router.replace('/about') } </script> <template> <div> <h1>User {{ userId }}</h1> <button @click="goToHome">Go Home</button> <button @click="goBack">Go Back</button> </div> </template>
State Management (Pinia)
Pinia is the official state management library for Vue 3, offering a simpler and more intuitive API than Vuex.
Creating a Store
import { defineStore } from 'pinia' // Option Store (similar to Options API) export const useCounterStore = defineStore('counter', { // State state: () => ({ count: 0, name: 'Counter' }), // Getters (computed) getters: { doubleCount: (state) => state.count * 2, doubleCountPlusOne() { return this.doubleCount + 1 } }, // Actions (methods) actions: { increment() { this.count++ }, decrement() { this.count-- }, async incrementAsync() { await new Promise(r => setTimeout(r, 1000)) this.count++ } } })
import { defineStore } from 'pinia' import { ref, computed } from 'vue' // Setup Store (Composition API style) export const useUserStore = defineStore('user', () => { // State const user = ref(null) const isLoading = ref(false) // Getters const isLoggedIn = computed(() => !!user.value) const fullName = computed(() => user.value ? `${user.value.firstName} ${user.value.lastName}` : '' ) // Actions async function login(credentials) { isLoading.value = true try { const response = await fetch('/api/login', { method: 'POST', body: JSON.stringify(credentials) }) user.value = await response.json() } finally { isLoading.value = false } } function logout() { user.value = null } return { user, isLoading, isLoggedIn, fullName, login, logout } })
Using Stores in Components
<script setup> import { storeToRefs } from 'pinia' import { useCounterStore } from '@/stores/counter' import { useUserStore } from '@/stores/user' const counterStore = useCounterStore() const userStore = useUserStore() // Destructure with storeToRefs to keep reactivity const { count, doubleCount } = storeToRefs(counterStore) const { increment, decrement } = counterStore const { user, isLoggedIn } = storeToRefs(userStore) </script> <template> <div> <h2>Count: {{ count }}</h2> <p>Double: {{ doubleCount }}</p> <button @click="increment">+</button> <button @click="decrement">-</button> <button @click="counterStore.incrementAsync()">Async +</button> <!-- Reset state --> <button @click="counterStore.$reset()">Reset</button> <!-- Patch state --> <button @click="counterStore.$patch({ count: 100 })">Set 100</button> <div v-if="isLoggedIn"> Welcome, {{ user.name }}! </div> </div> </template>
Lifecycle Hooks
<script setup> import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted, onActivated, onDeactivated, onErrorCaptured } from 'vue' // Before component is mounted to DOM onBeforeMount(() => { console.log('Before mount - DOM not ready') }) // Component is mounted - DOM is ready onMounted(() => { console.log('Mounted - DOM is ready!') // Fetch data, setup listeners, access DOM }) // Before reactive data update onBeforeUpdate(() => { console.log('Before update') }) // After reactive data update onUpdated(() => { console.log('Updated') }) // Before component is destroyed onBeforeUnmount(() => { console.log('Before unmount - cleanup!') // Remove event listeners, cancel timers }) // Component is destroyed onUnmounted(() => { console.log('Unmounted') }) // For keep-alive components onActivated(() => { console.log('Component activated') }) onDeactivated(() => { console.log('Component deactivated') }) // Error handling onErrorCaptured((error, instance, info) => { console.error('Error captured:', error) return false // Prevent error propagation }) </script>
<script> export default { beforeCreate() { console.log('beforeCreate') }, created() { console.log('created - data is reactive') }, beforeMount() { console.log('beforeMount') }, mounted() { console.log('mounted - DOM ready') }, beforeUpdate() { console.log('beforeUpdate') }, updated() { console.log('updated') }, beforeUnmount() { console.log('beforeUnmount') }, unmounted() { console.log('unmounted') } } </script>
API Integration
Fetching Data with Axios
import axios from 'axios' // Create axios instance const api = axios.create({ baseURL: 'https://api.example.com', timeout: 10000, headers: { 'Content-Type': 'application/json' } }) // Request interceptor api.interceptors.request.use( (config) => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }, (error) => Promise.reject(error) ) // Response interceptor api.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { // Handle unauthorized localStorage.removeItem('token') window.location.href = '/login' } return Promise.reject(error) } ) export default api
API Service & Component Usage
import api from './axios' export const userService = { getAll() { return api.get('/users') }, getById(id) { return api.get(`/users/${id}`) }, create(data) { return api.post('/users', data) }, update(id, data) { return api.put(`/users/${id}`, data) }, delete(id) { return api.delete(`/users/${id}`) } }
<script setup> import { ref, onMounted } from 'vue' import { userService } from '@/api/userService' const users = ref([]) const loading = ref(false) const error = ref(null) async function fetchUsers() { loading.value = true error.value = null try { const response = await userService.getAll() users.value = response.data } catch (err) { error.value = err.message } finally { loading.value = false } } async function deleteUser(id) { if (!confirm('Are you sure?')) return try { await userService.delete(id) users.value = users.value.filter(u => u.id !== id) } catch (err) { alert('Failed to delete user') } } onMounted(fetchUsers) </script> <template> <div class="users"> <h1>Users</h1> <!-- Loading state --> <div v-if="loading" class="loading"> <i data-lucide="loader-2" class="spin"></i> Loading... </div> <!-- Error state --> <div v-else-if="error" class="error"> <i data-lucide="alert-circle"></i> {{ error }} <button @click="fetchUsers">Retry</button> </div> <!-- Data --> <div v-else> <div v-for="user in users" :key="user.id" class="user-card"> <h3>{{ user.name }}</h3> <p>{{ user.email }}</p> <button @click="deleteUser(user.id)"> <i data-lucide="trash-2"></i> </button> </div> <p v-if="users.length === 0">No users found.</p> </div> </div> </template>
Pro Tips
Error Handling: Always handle loading and error states.
Composables: Extract API logic into reusable composables.
Caching: Use libraries like TanStack Query for advanced caching.
Congratulations!
You've completed the Vue.js guide! You're now equipped to build amazing frontend applications. Keep practicing and building projects!