The Vue 3 Composition API Cheat Sheet I Wish I Had on Day One
A practical Vue 3 cheat sheet for the Composition API — ref vs reactive, computed, watch, lifecycle hooks, props and emits, and template directives like v-if, v-for, and v-model.
The Vue 3 Composition API Cheat Sheet I Wish I Had on Day One
I spent my first week on Vue 3 typing mounted() into files that no longer had a mounted(). The reactivity model is genuinely better than Vue 2, but the muscle memory you build over years of Options API works against you for a few days. This is the quick reference I assembled to get past that — ref and reactive, computed, watch, the lifecycle hooks, props and emits, and the template directives — with a small worked example at the end. For the full searchable version with 80+ copyable snippets and side-by-side Options API equivalents, keep the Vue 3 Cheatsheet open in a tab.
Reactivity: ref and reactive
Everything in Vue 3 starts here. Two functions make state reactive, and choosing between them trips up almost everyone.
import { ref, reactive } from 'vue'
const count = ref(0) // primitive — access with count.value
const user = reactive({ // object — access fields directly
name: 'Li Lei',
role: 'frontend'
})
count.value++ // 1
user.role = 'lead' // tracked, no .value needed
The rule I settled on: ref for primitives and for anything you might replace wholesale (an API response, a list you reload on each navigation); reactive for object state you mutate in place (a form, settings, a draft). When in doubt, reach for ref — it can wrap anything, while reactive only accepts objects, arrays, Map, and Set.
The single biggest trap is destructuring. const { name } = user silently drops reactivity — the extracted variable is a plain string, disconnected from the proxy. Use toRefs to keep the link:
import { toRefs } from 'vue'
const { name, role } = toRefs(user) // both stay reactive
computed and watch
computed derives state that updates automatically and caches until its dependencies change. watch runs a side effect when a source changes.
import { ref, computed, watch } from 'vue'
const price = ref(100)
const qty = ref(2)
const total = computed(() => price.value * qty.value) // 200, cached
watch(qty, (next, prev) => {
console.log(`qty went ${prev} -> ${next}`)
})
The classic "my watcher never fires" bug is a getter problem. watch(obj.x, cb) passes the unwrapped value at that moment — a number, not a tracked source — so Vue has nothing to observe. Wrap it in a getter:
watch(() => obj.x, cb) // tracks obj.x
watch(items, cb, { deep: true }) // tracks nested mutations
Use watchEffect when you want the effect to run immediately and auto-collect whatever reactive values it touches, no explicit source list required.
Lifecycle hooks
In <script setup> there is no this and no options object — lifecycle hooks are functions you import and call. Each maps cleanly onto its Options API twin.
import { onMounted, onUpdated, onUnmounted } from 'vue'
onMounted(() => {
// mounted() — DOM is ready, fetch data, attach listeners
})
onUpdated(() => {
// updated() — runs after a reactive change re-renders
})
onUnmounted(() => {
// unmounted() — clean up timers, subscriptions, listeners
})
Coming from Vue 2, the mapping is the whole game: created/beforeMount work moves into setup itself (it runs before mount), mounted() becomes onMounted, and beforeDestroy/destroyed become onBeforeUnmount/onUnmounted. If you are also juggling React on another project, the React Hooks Cheatsheet covers the equivalent useEffect cleanup pattern.
Props and emits
With <script setup>, defineProps and defineEmits are compiler macros — you do not import them, and they give you typed, declarative component contracts.
<script setup>
const props = defineProps({
label: { type: String, required: true },
count: { type: Number, default: 0 }
})
const emit = defineEmits(['increment', 'reset'])
function bump() {
emit('increment', props.count + 1)
}
</script>
If you use TypeScript, the type-only form is cleaner and gives you better inference:
<script setup lang="ts">
const props = defineProps<{ label: string; count?: number }>()
const emit = defineEmits<{ increment: [value: number] }>()
</script>
Props are read-only by design — never assign to props.count inside the child. Emit an event and let the parent own the change. For two-way binding, defineModel (Vue 3.4+) collapses the old modelValue + update:modelValue ceremony into one line.
Template directives: v-if, v-for, v-model
The directives are mostly familiar from Vue 2, with one precedence change worth tattooing on your wrist.
<template>
<p v-if="count > 0">In stock</p>
<p v-else>Sold out</p>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
<input v-model="search" />
<input v-model.number="qty" />
<input v-model.trim="name" />
</template>
Always give v-for a stable :key — Vue uses it to track which DOM nodes to reuse, and skipping it causes subtle render bugs in lists that reorder. The flipped rule: in Vue 3, when v-if and v-for sit on the same element, v-if now has higher priority and evaluates first (the reverse of Vue 2). The fix is the same advice either way — do not combine them on one element. Filter the list in a computed instead, then v-for over the result.
v-model modifiers do real work: .number casts the input to a number, .trim strips surrounding whitespace, and .lazy syncs on change instead of every keystroke.
A worked example: a reactive counter
Here is the smallest complete component that ties reactivity, computed, a lifecycle hook, and a template directive together — the "hello world" of <script setup>.
<script setup>
import { ref, computed, onMounted } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
onMounted(() => {
console.log('Counter mounted, starting at', count.value)
})
</script>
<template>
<button @click="increment">Count is {{ count }}</button>
<p v-if="count > 0">Doubled: {{ doubled }}</p>
</template>
That is the entire mental model in twelve lines. count is reactive state, doubled derives from it and recomputes only when count changes, increment mutates count.value, onMounted logs once the DOM exists, and the template wires @click to the handler with a v-if guard on the derived output. No this, no data(), no methods bucket — just plain functions and variables in the order you think about them.
Where this saves you time
The reason I keep a cheat sheet open rather than relying on memory is that the failure modes are quiet. A watcher that never fires does not throw — it just leaves a form unvalidated. A destructured reactive object does not warn — it just stops updating. An async setup with no <Suspense> boundary renders a blank page with no error in the console. Each of those cost me real debugging hours before they cost me one search. Bookmark the Vue 3 Cheatsheet, search "watch" or "reactive" when something feels off, and copy the snippet that already accounts for the trap. The whole sheet runs in your browser with nothing uploaded, so it works behind a corporate proxy or on an air-gapped machine just as well.
Made by Toolora · Updated 2026-06-13