A comprehensive reference for Vue.js - a progressive JavaScript framework for building user interfaces with reactive data binding and component-based architecture.
- Basic Setup
- Vue Instance
- Template Syntax
- Directives
- Components
- Props and Events
- Composition API
- Reactivity
- Computed Properties
- Watchers
- Lifecycle Hooks
- Vue Router
- Vuex/Pinia State Management
- Forms
- Transitions
- Best Practices
# Vue CLI
npm install -g @vue/cli
vue create my-project
# Vite (recommended)
npm create vue@latest my-project
# CDN
<script src="https://unpkg.com/vue@next"></script>
// main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
// Options API
const { createApp } = Vue
createApp({
data() {
return {
message: 'Hello Vue!',
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}).mount('#app')
<template>
<!-- Text interpolation -->
<p>{{ message }}</p>
<!-- Raw HTML -->
<p v-html="rawHtml"></p>
<!-- Attributes -->
<div v-bind:id="dynamicId"></div>
<div :id="dynamicId"></div> <!-- shorthand -->
<!-- JavaScript expressions -->
<p>{{ count + 1 }}</p>
<p>{{ message.split('').reverse().join('') }}</p>
<p>{{ user ? user.name : 'Guest' }}</p>
</template>
<template>
<!-- v-if conditional rendering -->
<p v-if="show">Visible when show is true</p>
<p v-else-if="maybeShow">Maybe visible</p>
<p v-else>Fallback content</p>
<!-- v-show (CSS display toggle) -->
<p v-show="isVisible">Toggle visibility</p>
<!-- v-for list rendering -->
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
<!-- v-for with index -->
<li v-for="(item, index) in items" :key="item.id">
{{ index }} - {{ item.name }}
</li>
<!-- v-for with objects -->
<li v-for="(value, key) in user" :key="key">
{{ key }}: {{ value }}
</li>
<!-- v-on event handling -->
<button v-on:click="handleClick">Click me</button>
<button @click="handleClick">Click me (shorthand)</button>
<input @input="handleInput" @keyup.enter="handleEnter" />
<!-- v-model two-way binding -->
<input v-model="message" type="text" />
<textarea v-model="text"></textarea>
<input v-model="checked" type="checkbox" />
<select v-model="selected">
<option value="a">A</option>
<option value="b">B</option>
</select>
<!-- v-bind class and style -->
<div :class="{ active: isActive, 'text-danger': hasError }">
<div :class="[activeClass, errorClass]">
<div :style="{ color: textColor, fontSize: fontSize + 'px' }">
<!-- v-pre -->
<span v-pre>{{ this will not be compiled }}</span>
<!-- v-once -->
<h1 v-once>{{ title }}</h1>
</template>
// Global directive
app.directive('focus', {
mounted(el) {
el.focus()
}
})
// Local directive
directives: {
focus: {
mounted(el) {
el.focus()
}
}
}
// Usage
<input v-focus />
<template>
<div class="greeting">
<h1>{{ title }}</h1>
<p>{{ message }}</p>
<button @click="updateMessage">Update</button>
</div>
</template>
<script>
export default {
name: 'Greeting',
data() {
return {
title: 'Hello Vue!',
message: 'Welcome to Vue.js'
}
},
methods: {
updateMessage() {
this.message = 'Message updated!'
}
}
}
</script>
<style scoped>
.greeting {
padding: 20px;
background: #f0f0f0;
}
h1 {
color: #42b883;
}
</style>
// Global registration
import MyComponent from './components/MyComponent.vue'
app.component('MyComponent', MyComponent)
// Local registration
export default {
components: {
MyComponent
}
}
<!-- Parent Component -->
<template>
<ChildComponent
:title="parentTitle"
:user="currentUser"
:items="list"
/>
</template>
<!-- Child Component -->
<script>
export default {
props: {
title: String,
user: Object,
items: Array
},
// Or with validation
props: {
title: {
type: String,
required: true,
default: 'Default Title'
},
age: {
type: Number,
validator(value) {
return value >= 0
}
}
}
}
</script>
<!-- Child Component -->
<template>
<button @click="handleClick">Click me</button>
</template>
<script>
export default {
emits: ['custom-event'],
methods: {
handleClick() {
this.$emit('custom-event', 'Hello from child!')
}
}
}
</script>
<!-- Parent Component -->
<template>
<ChildComponent @custom-event="handleCustomEvent" />
</template>
<script>
export default {
methods: {
handleCustomEvent(message) {
console.log(message)
}
}
}
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
<p>Double: {{ doubleCount }}</p>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue'
export default {
setup() {
// Reactive state
const count = ref(0)
// Computed property
const doubleCount = computed(() => count.value * 2)
// Method
const increment = () => {
count.value++
}
// Lifecycle hook
onMounted(() => {
console.log('Component mounted')
})
// Return to template
return {
count,
doubleCount,
increment
}
}
}
</script>
<template>
<div>
<p>{{ message }}</p>
<input v-model="inputValue" />
<UserCard :user="user" @update="handleUpdate" />
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import UserCard from './UserCard.vue'
// Props
const props = defineProps({
initialMessage: String
})
// Emits
const emit = defineEmits(['change'])
// Reactive state
const message = ref(props.initialMessage || 'Hello')
const inputValue = ref('')
const user = ref({ name: 'John', age: 30 })
// Computed
const uppercaseMessage = computed(() => message.value.toUpperCase())
// Watcher
watch(inputValue, (newValue) => {
console.log('Input changed:', newValue)
emit('change', newValue)
})
// Methods
const handleUpdate = (data) => {
user.value = { ...user.value, ...data }
}
// Lifecycle
onMounted(() => {
console.log('Component mounted')
})
</script>
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initialValue
const isEven = computed(() => count.value % 2 === 0)
return {
count,
increment,
decrement,
reset,
isEven
}
}
// Usage in component
<script setup>
import { useCounter } from '@/composables/useCounter'
const { count, increment, decrement, isEven } = useCounter(10)
</script>
import { ref, reactive, toRefs } from 'vue'
export default {
setup() {
// ref for primitives
const count = ref(0)
const message = ref('Hello')
// reactive for objects
const state = reactive({
user: { name: 'John', age: 30 },
items: [1, 2, 3]
})
// Convert reactive object to refs
const { user, items } = toRefs(state)
return {
count,
message,
state,
user,
items
}
}
}
export default {
data() {
return {
firstName: 'John',
lastName: 'Doe'
}
},
computed: {
fullName() {
return `${this.firstName} ${this.lastName}`
},
// Computed with getter and setter
fullNameGS: {
get() {
return `${this.firstName} ${this.lastName}`
},
set(value) {
const [firstName, lastName] = value.split(' ')
this.firstName = firstName
this.lastName = lastName
}
}
}
}
import { ref, computed } from 'vue'
export default {
setup() {
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
// Writable computed
const fullNameWritable = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (value) => {
const [first, last] = value.split(' ')
firstName.value = first
lastName.value = last
}
})
return { firstName, lastName, fullName, fullNameWritable }
}
}
export default {
data() {
return {
question: '',
answer: 'I cannot answer until you ask a question!'
}
},
watch: {
// Simple watcher
question(newQuestion, oldQuestion) {
if (newQuestion.includes('?')) {
this.getAnswer()
}
},
// Deep watcher for objects
user: {
handler(newUser, oldUser) {
console.log('User changed:', newUser)
},
deep: true
},
// Immediate execution
searchTerm: {
handler(newTerm) {
this.performSearch(newTerm)
},
immediate: true
}
}
}
import { ref, watch, watchEffect } from 'vue'
export default {
setup() {
const count = ref(0)
const user = ref({ name: 'John', age: 30 })
// Basic watcher
watch(count, (newCount, oldCount) => {
console.log(`Count changed from ${oldCount} to ${newCount}`)
})
// Watch multiple sources
watch([count, user], ([newCount, newUser], [oldCount, oldUser]) => {
console.log('Count or user changed')
})
// Deep watcher
watch(user, (newUser) => {
console.log('User changed:', newUser)
}, { deep: true })
// watchEffect - automatically tracks dependencies
watchEffect(() => {
console.log(`Count is ${count.value}`)
})
return { count, user }
}
}
// Options API
export default {
beforeCreate() {
console.log('beforeCreate')
},
created() {
console.log('created')
},
beforeMount() {
console.log('beforeMount')
},
mounted() {
console.log('mounted')
},
beforeUpdate() {
console.log('beforeUpdate')
},
updated() {
console.log('updated')
},
beforeUnmount() {
console.log('beforeUnmount')
},
unmounted() {
console.log('unmounted')
}
}
// Composition API
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
export default {
setup() {
onBeforeMount(() => {
console.log('beforeMount')
})
onMounted(() => {
console.log('mounted')
})
onBeforeUpdate(() => {
console.log('beforeUpdate')
})
onUpdated(() => {
console.log('updated')
})
onBeforeUnmount(() => {
console.log('beforeUnmount')
})
onUnmounted(() => {
console.log('unmounted')
})
}
}
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'
const routes = [
{ path: '/', name: 'Home', component: Home },
{ path: '/about', name: 'About', component: About },
{ path: '/user/:id', name: 'User', component: User },
{
path: '/admin',
component: Admin,
beforeEnter: (to, from, next) => {
// Route guard
if (isAuthenticated) {
next()
} else {
next('/login')
}
}
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
// main.js
import { createApp } from 'vue'
import router from './router'
import App from './App.vue'
createApp(App).use(router).mount('#app')
<template>
<div>
<!-- Router links -->
<router-link to="/">Home</router-link>
<router-link :to="{ name: 'User', params: { id: 123 } }">User</router-link>
<!-- Router view -->
<router-view />
</div>
</template>
<script>
export default {
methods: {
navigateToUser() {
// Programmatic navigation
this.$router.push('/user/456')
this.$router.push({ name: 'User', params: { id: 456 } })
this.$router.replace('/new-path')
this.$router.go(-1) // Go back
}
},
created() {
// Access route information
console.log(this.$route.params.id)
console.log(this.$route.query)
console.log(this.$route.path)
}
}
</script>
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Eduardo'
}),
getters: {
doubleCount: (state) => state.count * 2,
getUserById: (state) => {
return (userId) => state.users.find(user => user.id === userId)
}
},
actions: {
increment() {
this.count++
},
async fetchUser(id) {
const user = await api.getUser(id)
this.users.push(user)
}
}
})
// Component usage
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
// Access state
console.log(counter.count)
// Access getters
console.log(counter.doubleCount)
// Call actions
counter.increment()
</script>
// store/index.js
import { createStore } from 'vuex'
export default createStore({
state: {
count: 0,
user: null
},
mutations: {
INCREMENT(state) {
state.count++
},
SET_USER(state, user) {
state.user = user
}
},
actions: {
async fetchUser({ commit }, userId) {
const user = await api.getUser(userId)
commit('SET_USER', user)
}
},
getters: {
doubleCount: state => state.count * 2,
isLoggedIn: state => !!state.user
}
})
// Component usage
export default {
computed: {
count() {
return this.$store.state.count
},
doubleCount() {
return this.$store.getters.doubleCount
}
},
methods: {
increment() {
this.$store.commit('INCREMENT')
},
fetchUser(id) {
this.$store.dispatch('fetchUser', id)
}
}
}
<template>
<form @submit.prevent="handleSubmit">
<!-- Text input -->
<input
v-model="form.name"
type="text"
placeholder="Name"
:class="{ error: errors.name }"
/>
<span v-if="errors.name" class="error">{{ errors.name }}</span>
<!-- Email input -->
<input
v-model="form.email"
type="email"
placeholder="Email"
/>
<!-- Checkbox -->
<label>
<input v-model="form.subscribe" type="checkbox" />
Subscribe to newsletter
</label>
<!-- Radio buttons -->
<div>
<label>
<input v-model="form.gender" value="male" type="radio" />
Male
</label>
<label>
<input v-model="form.gender" value="female" type="radio" />
Female
</label>
</div>
<!-- Select -->
<select v-model="form.country">
<option value="">Select Country</option>
<option v-for="country in countries" :key="country.code" :value="country.code">
{{ country.name }}
</option>
</select>
<!-- Multiple select -->
<select v-model="form.skills" multiple>
<option value="js">JavaScript</option>
<option value="vue">Vue.js</option>
<option value="react">React</option>
</select>
<!-- File input -->
<input @change="handleFileChange" type="file" accept="image/*" />
<button type="submit" :disabled="!isFormValid">Submit</button>
</form>
</template>
<script setup>
import { reactive, computed } from 'vue'
const form = reactive({
name: '',
email: '',
subscribe: false,
gender: '',
country: '',
skills: [],
file: null
})
const errors = reactive({})
const countries = ref([
{ code: 'us', name: 'United States' },
{ code: 'ca', name: 'Canada' }
])
const isFormValid = computed(() => {
return form.name && form.email && !Object.keys(errors).length
})
const validateField = (field, value) => {
switch (field) {
case 'name':
if (!value) {
errors.name = 'Name is required'
} else {
delete errors.name
}
break
case 'email':
if (!value || !value.includes('@')) {
errors.email = 'Valid email is required'
} else {
delete errors.email
}
break
}
}
const handleFileChange = (event) => {
form.file = event.target.files[0]
}
const handleSubmit = async () => {
// Validate form
validateField('name', form.name)
validateField('email', form.email)
if (!isFormValid.value) return
try {
await submitForm(form)
// Reset form
Object.assign(form, {
name: '',
email: '',
subscribe: false,
gender: '',
country: '',
skills: [],
file: null
})
} catch (error) {
console.error('Submit error:', error)
}
}
</script>
<template>
<div>
<button @click="show = !show">Toggle</button>
<!-- Basic transition -->
<transition name="fade">
<p v-if="show">Hello Vue!</p>
</transition>
<!-- List transitions -->
<transition-group name="list" tag="ul">
<li v-for="item in items" :key="item.id" class="list-item">
{{ item.text }}
</li>
</transition-group>
</div>
</template>
<script setup>
import { ref } from 'vue'
const show = ref(true)
const items = ref([
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' }
])
</script>
<style>
/* Fade transition */
.fade-enter-active, .fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
/* List transitions */
.list-enter-active {
transition: all 0.5s ease;
}
.list-leave-active {
transition: all 0.5s ease;
position: absolute;
}
.list-enter-from {
opacity: 0;
transform: translateX(30px);
}
.list-leave-to {
opacity: 0;
transform: translateX(-30px);
}
.list-move {
transition: transform 0.5s ease;
}
</style>
<!-- Good: Single File Component Structure -->
<template>
<!-- Use semantic HTML -->
<article class="user-card">
<header class="user-card__header">
<h2>{{ user.name }}</h2>
</header>
<div class="user-card__content">
<UserAvatar :src="user.avatar" :size="'medium'" />
<UserDetails :user="user" />
</div>
</article>
</template>
<script setup>
// Import statements
import { computed } from 'vue'
import UserAvatar from './UserAvatar.vue'
import UserDetails from './UserDetails.vue'
// Props with validation
const props = defineProps({
user: {
type: Object,
required: true,
validator: (user) => user && user.id && user.name
}
})
// Emits declaration
const emit = defineEmits(['update-user', 'delete-user'])
// Computed properties
const userInitials = computed(() => {
return props.user.name.split(' ').map(n => n[0]).join('')
})
</script>
<style scoped>
.user-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 1rem;
}
.user-card__header {
margin-bottom: 1rem;
}
</style>
// Use shallowRef for large objects that don't need deep reactivity
import { shallowRef } from 'vue'
const largeObject = shallowRef({ /* large data */ })
// Use markRaw for objects that should never be reactive
import { markRaw } from 'vue'
const nonReactiveObject = markRaw({ /* data */ })
// Lazy load components
const LazyComponent = defineAsyncComponent(() => import('./LazyComponent.vue'))
Pattern | Use Case | Example |
---|---|---|
Slot Pattern | Flexible content distribution | <slot name="header" /> |
Provide/Inject | Dependency injection | provide('theme', theme) |
Mixins | Code reuse (Options API) | mixins: [validationMixin] |
Composables | Logic reuse (Composition API) | useCounter() , useApi() |
# Development
npm run dev
# Production build
npm run build
# Preview production build
npm run preview
# Environment variables
VITE_API_URL=https://api.example.com npm run build
- Official Vue.js Documentation
- Vue 3 Composition API
- Vue Router
- Pinia State Management
- Vue DevTools
- Vite Build Tool
Originally compiled from various sources. Contributions welcome!