Back to Subjects
Vue.js Cheatsheet
Progressive framework for building user interfaces with Vue 3 and Composition API. Complete Vue.js reference guide.
Vue.js 3 Cheatsheet
Installation & Setup
CDN
<script src="https://unpkg.com/vue@next"></script>
NPM/Yarn
npm create vue@latest my-project
cd my-project
npm install
npm run dev
# Or with yarn
yarn create vue my-project
cd my-project
yarn install
yarn dev
Vue CLI
npm install -g @vue/cli
vue create my-project
cd my-project
npm run serve
Vue 3 Composition API
Basic Component
<template>
<div>
<h1>{{ title }}</h1>
<p>Count: {{ count }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue'
export default {
name: 'Counter',
setup() {
// Reactive data
const count = ref(0)
const title = ref('Vue 3 Counter')
// Computed property
const doubleCount = computed(() => count.value * 2)
// Methods
const increment = () => {
count.value++
}
const decrement = () => {
count.value--
}
// Lifecycle hooks
onMounted(() => {
console.log('Component mounted')
})
// Return everything that should be available in template
return {
count,
title,
doubleCount,
increment,
decrement
}
}
}
</script>
Script Setup (Simplified Syntax)
<template>
<div>
<h1>{{ title }}</h1>
<p>Count: {{ count }}</p>
<button @click="increment">+</button>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
// Reactive data
const count = ref(0)
const title = ref('Vue 3 Counter')
// Computed property
const doubleCount = computed(() => count.value * 2)
// Methods
const increment = () => {
count.value++
}
// Lifecycle hooks
onMounted(() => {
console.log('Component mounted')
})
</script>
Reactivity
ref() - Primitive Values
import { ref } from 'vue'
const count = ref(0)
const message = ref('Hello')
const isVisible = ref(true)
// Access value with .value
console.log(count.value) // 0
count.value = 10
reactive() - Objects
import { reactive } from 'vue'
const state = reactive({
count: 0,
user: {
name: 'John',
email: 'john@example.com'
}
})
// Direct property access
console.log(state.count) // 0
state.count = 10
state.user.name = 'Jane'
toRefs() - Convert Reactive Object
import { reactive, toRefs } from 'vue'
const state = reactive({
count: 0,
message: 'Hello'
})
// Convert to refs for destructuring
const { count, message } = toRefs(state)
// Now count and message are refs
console.log(count.value) // 0
readonly() - Immutable Data
import { reactive, readonly } from 'vue'
const state = reactive({
count: 0
})
const readonlyState = readonly(state)
// readonlyState.count = 10 // Warning: cannot modify
Computed Properties
Basic Computed
import { ref, computed } from 'vue'
const count = ref(1)
const doubleCount = computed(() => count.value * 2)
console.log(doubleCount.value) // 2
Computed with Getter and Setter
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed({
get() {
return firstName.value + ' ' + lastName.value
},
set(newValue) {
[firstName.value, lastName.value] = newValue.split(' ')
}
})
fullName.value = 'Jane Smith'
console.log(firstName.value) // Jane
console.log(lastName.value) // Smith
Watchers
watch() - Watch Reactive Data
import { ref, watch } from 'vue'
const count = ref(0)
// Watch a single ref
watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`)
})
// Watch multiple sources
const name = ref('John')
const age = ref(30)
watch([name, age], ([newName, newAge], [oldName, oldAge]) => {
console.log(`Name: ${oldName} -> ${newName}`)
console.log(`Age: ${oldAge} -> ${newAge}`)
})
watchEffect() - Automatic Dependency Tracking
import { ref, watchEffect } from 'vue'
const count = ref(0)
const message = ref('Hello')
watchEffect(() => {
// Automatically tracks count and message
console.log(`${message.value}: ${count.value}`)
})
Watch Options
import { ref, watch } from 'vue'
const obj = ref({ count: 0 })
// Deep watch for objects
watch(obj, (newValue, oldValue) => {
console.log('Object changed')
}, { deep: true })
// Immediate execution
watch(count, (newValue, oldValue) => {
console.log('Count changed')
}, { immediate: true })
// Flush timing
watch(count, (newValue, oldValue) => {
console.log('Count changed')
}, { flush: 'post' }) // 'pre', 'post', or 'sync'
Lifecycle Hooks
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onActivated,
onDeactivated,
onErrorCaptured
} from 'vue'
export default {
setup() {
onBeforeMount(() => {
console.log('Before mount')
})
onMounted(() => {
console.log('Mounted')
})
onBeforeUpdate(() => {
console.log('Before update')
})
onUpdated(() => {
console.log('Updated')
})
onBeforeUnmount(() => {
console.log('Before unmount')
})
onUnmounted(() => {
console.log('Unmounted')
})
onActivated(() => {
console.log('Activated (keep-alive)')
})
onDeactivated(() => {
console.log('Deactivated (keep-alive)')
})
onErrorCaptured((error, instance, info) => {
console.log('Error captured:', error)
})
}
}
Template Syntax
Text Interpolation
<template>
<!-- Text interpolation -->
<span>Message: {{ msg }}</span>
<!-- Raw HTML -->
<span v-html="rawHtml"></span>
<!-- Attribute binding -->
<div v-bind:id="dynamicId"></div>
<div :id="dynamicId"></div>
<div :class="{ active: isActive }"></div>
<!-- JavaScript expressions -->
<span>{{ number + 1 }}</span>
<span>{{ ok ? 'YES' : 'NO' }}</span>
<span>{{ message.split('').reverse().join('') }}</span>
</template>
Directives
<template>
<!-- v-if / v-else-if / v-else -->
<div v-if="type === 'A'">A</div>
<div v-else-if="type === 'B'">B</div>
<div v-else>Not A/B</div>
<!-- v-show -->
<div v-show="isVisible">Visible</div>
<!-- v-for -->
<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 object -->
<li v-for="(value, key) in object" :key="key">
{{ key }}: {{ value }}
</li>
<!-- v-on (event handling) -->
<button v-on:click="doSomething">Click me</button>
<button @click="doSomething">Click me</button>
<button @click="count++">Increment</button>
<!-- Event modifiers -->
<form @submit.prevent="onSubmit">
<input @keyup.enter="submit">
<button @click.once="doOnce">Do once</button>
</form>
<!-- v-model (two-way binding) -->
<input v-model="message" placeholder="Edit me">
<p>Message is: {{ message }}</p>
</template>
Class and Style Binding
<template>
<!-- Class binding -->
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
<div :class="[activeClass, errorClass]"></div>
<div :class="classObject"></div>
<!-- Style binding -->
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
<div :style="[baseStyles, overridingStyles]"></div>
<div :style="styleObject"></div>
</template>
<script setup>
import { ref, computed } from 'vue'
const isActive = ref(true)
const hasError = ref(false)
const activeClass = ref('active')
const errorClass = ref('text-danger')
const classObject = computed(() => ({
active: isActive.value && !hasError.value,
'text-danger': hasError.value
}))
const activeColor = ref('red')
const fontSize = ref(30)
const styleObject = ref({
color: 'red',
fontSize: '13px'
})
</script>
Component Communication
Props
<!-- Parent Component -->
<template>
<ChildComponent
:message="parentMessage"
:count="count"
:user="user"
/>
</template>
<!-- Child Component -->
<template>
<div>
<p>{{ message }}</p>
<p>Count: {{ count }}</p>
<p>User: {{ user.name }}</p>
</div>
</template>
<script setup>
// Define props
const props = defineProps({
message: String,
count: {
type: Number,
default: 0
},
user: {
type: Object,
required: true
}
})
// Or with TypeScript
const props = defineProps<{
message: string
count?: number
user: { name: string, email: string }
}>()
</script>
Emits
<!-- Child Component -->
<template>
<button @click="handleClick">Click me</button>
</template>
<script setup>
// Define emits
const emit = defineEmits(['update', 'delete'])
// Or with validation
const emit = defineEmits({
update: (value) => {
return typeof value === 'string'
},
delete: null
})
const handleClick = () => {
emit('update', 'Hello from child')
emit('delete', id)
}
</script>
<!-- Parent Component -->
<template>
<ChildComponent
@update="handleUpdate"
@delete="handleDelete"
/>
</template>
<script setup>
const handleUpdate = (value) => {
console.log('Updated:', value)
}
const handleDelete = (id) => {
console.log('Delete:', id)
}
</script>
Provide/Inject
<!-- Parent/Ancestor Component -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
const updateTheme = (newTheme) => {
theme.value = newTheme
}
// Provide data and methods
provide('theme', theme)
provide('updateTheme', updateTheme)
</script>
<!-- Child/Descendant Component -->
<script setup>
import { inject } from 'vue'
// Inject provided data
const theme = inject('theme')
const updateTheme = inject('updateTheme')
const changeTheme = () => {
updateTheme('light')
}
</script>
Slots
Basic Slots
<!-- Parent Component -->
<template>
<BaseCard>
<h3>Card Title</h3>
<p>Card content goes here</p>
</BaseCard>
</template>
<!-- BaseCard Component -->
<template>
<div class="card">
<slot></slot>
</div>
</template>
Named Slots
<!-- Parent Component -->
<template>
<BaseLayout>
<template #header>
<h1>Page Title</h1>
</template>
<template #default>
<p>Main content</p>
</template>
<template #footer>
<p>Footer content</p>
</template>
</BaseLayout>
</template>
<!-- BaseLayout Component -->
<template>
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
Scoped Slots
<!-- Parent Component -->
<template>
<UserList>
<template #default="{ user, index }">
<strong>{{ index + 1 }}. {{ user.name }}</strong>
<span>({{ user.email }})</span>
</template>
</UserList>
</template>
<!-- UserList Component -->
<template>
<div>
<div v-for="(user, index) in users" :key="user.id">
<slot :user="user" :index="index"></slot>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const users = ref([
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' }
])
</script>
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 }
]
export const router = createRouter({
history: createWebHistory(),
routes
})
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { router } from './router'
createApp(App).use(router).mount('#app')
Navigation
<template>
<!-- Router links -->
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
<router-link :to="{ name: 'user', params: { id: 123 }}">User</router-link>
<!-- Router view -->
<router-view />
</template>
<script setup>
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
// Programmatic navigation
const goToAbout = () => {
router.push('/about')
// router.push({ name: 'about' })
// router.push({ path: '/about', query: { tab: 'info' }})
}
// Access route parameters
console.log(route.params.id)
console.log(route.query.tab)
</script>
State Management (Pinia)
Installation
npm install pinia
Setup
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.mount('#app')
Store Definition
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
// State
const count = ref(0)
const name = ref('Eduardo')
// Getters (computed)
const doubleCount = computed(() => count.value * 2)
// Actions (methods)
function increment() {
count.value++
}
function decrement() {
count.value--
}
return {
count,
name,
doubleCount,
increment,
decrement
}
})
Using Store in Components
<template>
<div>
<p>Count: {{ counter.count }}</p>
<p>Double: {{ counter.doubleCount }}</p>
<button @click="counter.increment">+</button>
<button @click="counter.decrement">-</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
// Or destructure with storeToRefs
import { storeToRefs } from 'pinia'
const { count, doubleCount } = storeToRefs(counter)
const { increment, decrement } = counter
</script>
Useful Composables
useLocalStorage
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const storedValue = localStorage.getItem(key)
const value = ref(storedValue ? JSON.parse(storedValue) : defaultValue)
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
return value
}
// Usage
const theme = useLocalStorage('theme', 'light')
useFetch
import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(true)
fetch(url)
.then(response => response.json())
.then(json => {
data.value = json
})
.catch(err => {
error.value = err
})
.finally(() => {
loading.value = false
})
return { data, error, loading }
}
// Usage
const { data, error, loading } = useFetch('/api/users')
Best Practices
- Use Composition API - More flexible and reusable than Options API
- Prefer script setup - Cleaner syntax and better TypeScript support
- Use computed for derived state - More efficient than methods
- Destructure reactive objects with toRefs - Maintain reactivity
- Use provide/inject for deeply nested props - Avoid prop drilling
- Keep components small and focused - Single responsibility principle
- Use TypeScript - Better developer experience and catch errors early
- Follow naming conventions - PascalCase for components, kebab-case for props