Skip to content

Vue Quick Reference

A quick lookup for Vue 3 concepts and patterns grouped by category. All examples use the Composition API with <script setup> and TypeScript.

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'
// Props
const props = defineProps<{ name: string; age?: number }>()
// Emits
const emit = defineEmits<{ select: [id: string] }>()
// State
const count = ref(0)
// Derived
const doubled = computed(() => count.value * 2)
// Lifecycle
onMounted(() => console.log('mounted'))
</script>
<template>
<ChildComponent :name="props.name" @select="emit('select', $event)" />
<p>{{ count }} × 2 = {{ doubled }}</p>
<button @click="count++">+1</button>
</template>
<style scoped>
p { color: var(--accent); }
</style>

import { ref } from 'vue'
const count = ref(0) // number
const name = ref('Alice') // string
const list = ref<string[]>([])
// Script: access with .value
count.value++
name.value = 'Bob'
list.value.push('item')
// Template: auto-unwrapped — no .value needed
// {{ count }} {{ name }}
import { reactive } from 'vue'
const form = reactive({ name: '', age: 0 })
// Direct property access — no .value
form.name = 'Alice'
form.age = 30
// ❌ Don't destructure — loses reactivity
// const { name } = form
// ✅ Keep property access on the object, or use toRefs
import { computed } from 'vue'
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// Writable computed
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (val) => { [firstName.value, lastName.value] = val.split(' ') },
})
import { watch, watchEffect } from 'vue'
// Watch specific source — provides old and new values
watch(count, (newVal, oldVal) => {
console.log(newVal, oldVal)
})
// Watch getter expression
watch(() => route.params.id, (id) => loadPerson(id))
// Run immediately
watch(source, handler, { immediate: true })
// watchEffect — tracks all reactive reads automatically
watchEffect(() => {
document.title = `${name.value} — App`
})

<!-- Dynamic attribute -->
<img :src="url" :alt="caption" />
<!-- Shorthand for v-bind:attr -->
<div :class="{ active: isOn }" :style="{ color: accent }">
<!-- Event listener — shorthand for v-on:event -->
<button @click="handleClick">Click</button>
<form @submit.prevent="handleSubmit">...</form>
<input @keyup.enter="search" />
<!-- Inline handler -->
<button @click="count++">+1</button>
<button @click="emit('select', person.id)">Select</button>
<!-- Removes from DOM when false -->
<div v-if="isLoggedIn">Welcome</div>
<div v-else-if="isPending">Loading...</div>
<div v-else>Please log in</div>
<!-- Toggles display:none — element stays in DOM -->
<div v-show="isExpanded">Details</div>
<!-- Group without wrapper element -->
<template v-if="person">
<h2>{{ person.name }}</h2>
<p>{{ person.bio }}</p>
</template>
<!-- Always use :key with a stable unique id -->
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
<!-- With index -->
<li v-for="(item, index) in items" :key="item.id">{{ index }}. {{ item.name }}</li>
<!-- On components -->
<PersonCard
v-for="person in people"
:key="person.id"
:person="person"
@select="focusOn"
/>
<input v-model="name" type="text" />
<input v-model.trim="name" /> <!-- trims whitespace -->
<input v-model.number="age" /> <!-- converts to number -->
<input v-model.lazy="bio" /> <!-- syncs on change, not input -->
<input v-model="isChecked" type="checkbox" />
<select v-model="role">
<option value="admin">Admin</option>
</select>
<!-- Object syntax — keys are class names, values are booleans -->
<div :class="{ active: isActive, disabled: !canClick }">
<!-- Array syntax -->
<div :class="['btn', size, { primary: isPrimary }]">
<!-- Static + dynamic merge automatically -->
<div class="card" :class="{ highlighted }">
<!-- Style object -->
<div :style="{ color: textColor, fontSize: size + 'px' }">

// Props — TypeScript generic form
const props = defineProps<{
name: string
age?: number
role?: 'admin' | 'user'
}>()
// With defaults
const props = withDefaults(defineProps<{
role?: string
}>(), { role: 'user' })
// Emits
const emit = defineEmits<{
select: [id: string]
update: [value: string]
close: []
}>()
emit('select', person.id)
emit('close')
<!-- Passing props to a child -->
<PersonCard :name="person.name" :age="person.age" role="focus" />
<!-- Listening to emits -->
<PersonCard @select="handleSelect" @close="showPanel = false" />

<!-- Child defines the slot outlet -->
<template>
<div class="card">
<header><slot name="header" /></header>
<div class="body"><slot /></div>
</div>
</template>
<!-- Parent fills the slots -->
<Card>
<template #header><h2>Title</h2></template>
<p>Body content goes in the default slot.</p>
</Card>

import { onMounted, onUpdated, onUnmounted, onBeforeUnmount } from 'vue'
onMounted(() => {
// DOM is available — focus inputs, initialize libraries
})
onUnmounted(() => {
// Clean up timers, listeners, subscriptions
clearInterval(timer)
})

import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
// Read params and query
const id = route.params.id as string
const q = route.query.search as string
// Navigate
router.push({ name: 'person', params: { id } })
router.push({ name: 'home' })
router.replace({ name: 'edit', params: { id } })
<!-- RouterLink -->
<RouterLink to="/">Home</RouterLink>
<RouterLink :to="{ name: 'person', params: { id: person.id } }">
{{ person.name }}
</RouterLink>
<!-- Route outlet in App.vue -->
<RouterView />
// Route definition
{
path: '/:id',
name: 'person',
component: () => import('../views/PersonDetailView.vue'),
}
// Navigation guard
router.beforeEach((to, from) => {
if (!isAuthenticated()) return { name: 'login' }
})

// Store definition — setup syntax
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useFamilyStore = defineStore('family', () => {
// state
const people = ref<Person[]>([])
// getter
const count = computed(() => people.value.length)
// action
function addPerson(person: Person) {
people.value.push(person)
}
return { people, count, addPerson }
}, { persist: true })
// Using in a component
import { storeToRefs } from 'pinia'
import { useFamilyStore } from '../stores/familyStore'
const store = useFamilyStore()
// Reactive destructuring of state and getters
const { people, count } = storeToRefs(store)
// Actions destructure directly — no storeToRefs needed
const { addPerson } = store

<script setup lang="ts">
import { ref, onMounted } from 'vue'
const inputRef = ref<HTMLInputElement | null>(null)
onMounted(() => inputRef.value?.focus())
</script>
<template>
<input ref="inputRef" type="text" />
</template>
async function loadPeople() {
const response = await fetch('/api/people')
people.value = await response.json()
}
function handleDelete() {
if (!confirm('Delete?')) return
store.deletePerson(id)
router.push({ name: 'home' })
}
AppInput.vue
<script setup lang="ts">
const model = defineModel<string>()
</script>
<template>
<input :value="model" @input="model = $event.target.value" />
</template>
<!-- Parent -->
<AppInput v-model="personName" />