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.
Single-File Component Structure
Section titled “Single-File Component Structure”<script setup lang="ts">import { ref, computed, onMounted } from 'vue'import ChildComponent from './ChildComponent.vue'
// Propsconst props = defineProps<{ name: string; age?: number }>()
// Emitsconst emit = defineEmits<{ select: [id: string] }>()
// Stateconst count = ref(0)
// Derivedconst doubled = computed(() => count.value * 2)
// LifecycleonMounted(() => 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>Reactivity
Section titled “Reactivity”import { ref } from 'vue'
const count = ref(0) // numberconst name = ref('Alice') // stringconst list = ref<string[]>([])
// Script: access with .valuecount.value++name.value = 'Bob'list.value.push('item')
// Template: auto-unwrapped — no .value needed// {{ count }} {{ name }}reactive
Section titled “reactive”import { reactive } from 'vue'
const form = reactive({ name: '', age: 0 })
// Direct property access — no .valueform.name = 'Alice'form.age = 30
// ❌ Don't destructure — loses reactivity// const { name } = form
// ✅ Keep property access on the object, or use toRefscomputed
Section titled “computed”import { computed } from 'vue'
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// Writable computedconst fullName = computed({ get: () => `${firstName.value} ${lastName.value}`, set: (val) => { [firstName.value, lastName.value] = val.split(' ') },})watch / watchEffect
Section titled “watch / watchEffect”import { watch, watchEffect } from 'vue'
// Watch specific source — provides old and new valueswatch(count, (newVal, oldVal) => { console.log(newVal, oldVal)})
// Watch getter expressionwatch(() => route.params.id, (id) => loadPerson(id))
// Run immediatelywatch(source, handler, { immediate: true })
// watchEffect — tracks all reactive reads automaticallywatchEffect(() => { document.title = `${name.value} — App`})Template Directives
Section titled “Template Directives”Binding and events
Section titled “Binding and events”<!-- 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>Conditional rendering
Section titled “Conditional rendering”<!-- 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>List rendering
Section titled “List rendering”<!-- 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"/>Two-way binding
Section titled “Two-way binding”<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>Class and style bindings
Section titled “Class and style bindings”<!-- 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 and Emits
Section titled “Props and Emits”// Props — TypeScript generic formconst props = defineProps<{ name: string age?: number role?: 'admin' | 'user'}>()
// With defaultsconst props = withDefaults(defineProps<{ role?: string}>(), { role: 'user' })
// Emitsconst 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>Lifecycle Hooks
Section titled “Lifecycle Hooks”import { onMounted, onUpdated, onUnmounted, onBeforeUnmount } from 'vue'
onMounted(() => { // DOM is available — focus inputs, initialize libraries})
onUnmounted(() => { // Clean up timers, listeners, subscriptions clearInterval(timer)})Vue Router
Section titled “Vue Router”import { useRoute, useRouter } from 'vue-router'
const route = useRoute()const router = useRouter()
// Read params and queryconst id = route.params.id as stringconst q = route.query.search as string
// Navigaterouter.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 guardrouter.beforeEach((to, from) => { if (!isAuthenticated()) return { name: 'login' }})// Store definition — setup syntaximport { 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 componentimport { storeToRefs } from 'pinia'import { useFamilyStore } from '../stores/familyStore'
const store = useFamilyStore()
// Reactive destructuring of state and gettersconst { people, count } = storeToRefs(store)
// Actions destructure directly — no storeToRefs neededconst { addPerson } = storeCommon Patterns
Section titled “Common Patterns”Template ref (DOM access)
Section titled “Template ref (DOM access)”<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 action in store
Section titled “Async action in store”async function loadPeople() { const response = await fetch('/api/people') people.value = await response.json()}Conditional navigation after action
Section titled “Conditional navigation after action”function handleDelete() { if (!confirm('Delete?')) return store.deletePerson(id) router.push({ name: 'home' })}v-model on custom component (Vue 3.4+)
Section titled “v-model on custom component (Vue 3.4+)”<script setup lang="ts">const model = defineModel<string>()</script><template> <input :value="model" @input="model = $event.target.value" /></template>
<!-- Parent --><AppInput v-model="personName" />