Vue Composition API Cheat Sheet
Core syntax for Vue 3's Composition API including reactive refs, computed values, watchers, lifecycle hooks, and composables.
Reactive State with ref/reactive
Declare reactive primitives and objects inside <script setup>.
<script setup>import { ref, reactive, computed } from 'vue'const count = ref(0) // primitive -> access via count.valueconst user = reactive({ name: 'Ana', age: 30 }) // object -> no .valueconst doubled = computed(() => count.value * 2)function increment() { count.value++ user.age++}</script><template> <button @click="increment">{{ count }} / {{ doubled }}</button></template>
watch and watchEffect
React to reactive changes with fine-grained or automatic dependency tracking.
import { ref, watch, watchEffect } from 'vue'const search = ref('')const results = ref([])// watch: explicit source, gives old/new valueswatch(search, async (newVal, oldVal) => { results.value = await fetchResults(newVal)}, { immediate: true })// watchEffect: auto-tracks any reactive deps used insidewatchEffect(() => { console.log(`Searching for: ${search.value}`)})// stop a watcher manuallyconst stop = watch(search, () => {})stop()
Custom Composable
Extract stateful logic into a reusable function following the useX naming convention.
// useMouse.jsimport { ref, onMounted, onUnmounted } from 'vue'export function useMouse() { const x = ref(0) const y = ref(0) function update(e) { x.value = e.pageX y.value = e.pageY } onMounted(() => window.addEventListener('mousemove', update)) onUnmounted(() => window.removeEventListener('mousemove', update)) return { x, y }}// component.vue// import { useMouse } from './useMouse'// const { x, y } = useMouse()
defineProps / defineEmits
Type-safe props and events in <script setup> without imports.
<script setup lang="ts">interface Props { title: string count?: number}const props = withDefaults(defineProps<Props>(), { count: 0 })const emit = defineEmits<{ (e: 'update', value: number): void (e: 'close'): void}>()function bump() { emit('update', props.count + 1)}</script>
Composition API Lifecycle Hooks
Options API equivalents mapped to composition hooks.
- onMounted- runs after the component is mounted to the DOM (replaces mounted)
- onUpdated- runs after a reactive dependency triggers a re-render (replaces updated)
- onUnmounted- cleanup timers/listeners before the component is destroyed (replaces destroyed)
- onBeforeMount / onBeforeUpdate / onBeforeUnmount- pre-phase hooks for each lifecycle stage
- onErrorCaptured- catches errors from descendant components, return false to stop propagation
- onActivated / onDeactivated- fire when a component inside <KeepAlive> is toggled
Typed provide/inject
Share dependency-injected state safely across deeply nested components with compile-time key checking.
<script setup lang="ts">import { provide, inject, type InjectionKey, ref } from 'vue'interface Theme { mode: 'light' | 'dark' }const ThemeKey: InjectionKey<Theme> = Symbol('theme')// in an ancestor componentconst theme = ref<Theme>({ mode: 'dark' })provide(ThemeKey, theme.value)// in any descendant, with a required fallback to catch missing providersconst injectedTheme = inject(ThemeKey)if (!injectedTheme) throw new Error('Theme not provided')</script>
Custom Directive in script setup
Register a local vNNN-prefixed directive without a separate directives option.
<script setup>// any camelCase binding starting with v becomes a local custom directiveconst vFocus = { mounted: (el) => el.focus(),}const vClickOutside = { mounted(el, binding) { el._handler = (e) => { if (!el.contains(e.target)) binding.value(e) } document.addEventListener('click', el._handler) }, unmounted(el) { document.removeEventListener('click', el._handler) },}</script><template> <input v-focus /> <div v-click-outside="closeMenu">...</div></template>
shallowRef / shallowReactive for Performance
Skip deep reactivity conversion on large objects you replace wholesale instead of mutating in place.
import { shallowRef, triggerRef } from 'vue'// deep reactivity would recursively proxy every node of a huge dataset --// wasteful when you always swap the whole object rather than mutate pathsconst bigDataset = shallowRef(loadInitialDataset())async function refresh() { const next = await fetchLatestDataset() bigDataset.value = next // triggers because .value itself changed}function mutateInPlaceAndNotify() { bigDataset.value.rows.push(newRow) // NOT tracked (shallow) triggerRef(bigDataset) // manually force the update}
defineAsyncComponent + Suspense
Code-split a heavy component and coordinate its loading state declaratively with Suspense.
import { defineAsyncComponent } from 'vue'const HeavyChart = defineAsyncComponent({ loader: () => import('./HeavyChart.vue'), loadingComponent: LoadingSpinner, errorComponent: LoadError, delay: 200, timeout: 5000,})// template usage:// <Suspense>// <template #default><HeavyChart /></template>// <template #fallback>Loading chart...</template>// </Suspense>
Reactivity Gotchas
Edge cases that trip up developers who already know ref/reactive basics.
- Destructuring reactive() loses reactivity- use toRefs(state) before destructuring so each field stays a live ref
- Replacing a reactive() object breaks the binding- reassigning `state = newObj` orphans the original proxy; use Object.assign(state, newObj) instead
- ref() unwrapping only happens at the top level of templates/reactive objects- a ref nested inside an array keeps its .value wrapper
- watch on a reactive object is deep by default- but watch on a getter function `() => state.field` is shallow unless { deep: true } is set
- computed setters- computed({ get, set }) lets a computed value be assigned to, useful for two-way v-model bindings on derived state
- markRaw()- opts an object out of reactivity entirely, useful for large third-party class instances (chart libs, editors)
Prefer `reactive()` for grouped domain state and `ref()` for anything you'll destructure or pass across composable boundaries — destructuring a reactive object loses reactivity, but `toRefs()` fixes that when you must.