# Composition API Reference Deep-dive patterns for Vue 3 Composition API: composables, lifecycle, template refs, provide/inject, v-model, slots, transitions, Teleport, Suspense, and custom directives. --- ## Composables ### Naming and structure convention ```ts // composables/useCounter.ts import { ref, computed, onUnmounted } from 'vue' // Rule: always prefix with "use" export function useCounter(initialValue = 0) { // State: return refs so callers can destructure while keeping reactivity const count = ref(initialValue) const isNegative = computed(() => count.value < 0) function increment() { count.value++ } function decrement() { count.value-- } function reset() { count.value = initialValue } // Cleanup: always handle in onUnmounted if you register listeners/timers // (onUnmounted is a no-op when called outside a component) return { count, isNegative, increment, decrement, reset } } ``` ### Accepting refs as arguments (reactive composable inputs) ```ts // composables/useDouble.ts import { computed, toRef, MaybeRefOrGetter, toValue } from 'vue' // toValue() (Vue 3.3+) unwraps ref, getter, or raw value export function useDouble(value: MaybeRefOrGetter) { return computed(() => toValue(value) * 2) } // Usage: works with raw value, ref, or getter const x = ref(5) const doubled = useDouble(x) // reactive const doubled2 = useDouble(5) // static const doubled3 = useDouble(() => x.value + 1) // getter ``` ### useFetch — data fetching with cancellation ```ts // composables/useFetch.ts import { ref, watchEffect, toValue, MaybeRefOrGetter } from 'vue' export function useFetch(url: MaybeRefOrGetter) { const data = ref(null) const error = ref(null) const pending = ref(false) watchEffect((onCleanup) => { const controller = new AbortController() // Register cleanup BEFORE the async work onCleanup(() => controller.abort()) pending.value = true error.value = null fetch(toValue(url), { signal: controller.signal }) .then((res) => { if (!res.ok) throw new Error(`HTTP ${res.status}`) return res.json() }) .then((json) => { data.value = json }) .catch((err) => { if (err.name !== 'AbortError') error.value = err }) .finally(() => { pending.value = false }) }) return { data, error, pending } } ``` ### useLocalStorage — synced persistent state ```ts // composables/useLocalStorage.ts import { ref, watch } from 'vue' export function useLocalStorage(key: string, defaultValue: T) { const stored = localStorage.getItem(key) const initial = stored ? (JSON.parse(stored) as T) : defaultValue const state = ref(initial) watch( state, (value) => localStorage.setItem(key, JSON.stringify(value)), { deep: true } ) return state } // Usage const theme = useLocalStorage<'light' | 'dark'>('theme', 'light') ``` ### useEventListener — safe event binding ```ts // composables/useEventListener.ts import { onMounted, onUnmounted, isRef, watch } from 'vue' import type { Ref } from 'vue' export function useEventListener( target: Window | Document | Ref, event: K, handler: (e: WindowEventMap[K]) => void ) { if (isRef(target)) { watch(target, (el, _, onCleanup) => { el?.addEventListener(event, handler as EventListener) onCleanup(() => el?.removeEventListener(event, handler as EventListener)) }) } else { onMounted(() => target.addEventListener(event, handler as EventListener)) onUnmounted(() => target.removeEventListener(event, handler as EventListener)) } } // Usage useEventListener(window, 'resize', () => { console.log('window resized') }) ``` ### useDark — dark mode toggle ```ts // composables/useDark.ts import { ref, watch, onMounted } from 'vue' export function useDark() { const isDark = ref(false) onMounted(() => { isDark.value = document.documentElement.classList.contains('dark') || window.matchMedia('(prefers-color-scheme: dark)').matches }) watch(isDark, (dark) => { document.documentElement.classList.toggle('dark', dark) }) function toggle() { isDark.value = !isDark.value } return { isDark, toggle } } ``` ### useIntersectionObserver — lazy loading / scroll tracking ```ts // composables/useIntersectionObserver.ts import { ref, onMounted, onUnmounted } from 'vue' import type { Ref } from 'vue' export function useIntersectionObserver( target: Ref, options: IntersectionObserverInit = {} ) { const isIntersecting = ref(false) let observer: IntersectionObserver | null = null onMounted(() => { observer = new IntersectionObserver(([entry]) => { isIntersecting.value = entry.isIntersecting }, options) if (target.value) observer.observe(target.value) }) onUnmounted(() => observer?.disconnect()) return { isIntersecting } } // Usage const el = ref(null) const { isIntersecting } = useIntersectionObserver(el, { threshold: 0.1 }) ``` --- ## Lifecycle Hooks ```ts import { onBeforeMount, // before first render, DOM not yet created onMounted, // after first render, DOM available onBeforeUpdate, // before re-render triggered by reactive change onUpdated, // after re-render (DOM updated) onBeforeUnmount, // before component teardown (still fully functional) onUnmounted, // after component teardown onActivated, // component re-activated inside onDeactivated, // component deactivated inside onErrorCaptured, // error from descendant component } from 'vue' // Pattern: separate concerns into multiple onMounted calls onMounted(() => { initChart() }) onMounted(() => { attachKeyboardListeners() }) // KeepAlive lifecycle — fetch fresh data on each activation onActivated(() => { refreshData() }) onDeactivated(() => { pauseAnimations() }) // Error boundary at composable level onErrorCaptured((err, instance, info) => { logError(err) return false // prevent propagation }) ``` --- ## Template Refs ### Basic ref() approach ```vue ``` ### useTemplateRef() — Vue 3.5+ ```vue ``` ### Component refs — accessing exposed methods ```vue ``` ```vue ``` ### Dynamic template refs in v-for ```vue ``` --- ## provide / inject ### Typed injection keys (InjectionKey) ```ts // keys/injection-keys.ts import { InjectionKey, Ref } from 'vue' export interface UserContext { user: Ref logout: () => void } // The key carries the type — no casts needed at inject site export const UserContextKey: InjectionKey = Symbol('UserContext') ``` ### Providing values (ancestor component) ```vue ``` ### Injecting in descendants ```vue ``` --- ## v-model Patterns ### defineModel() — Vue 3.4+ ```vue ``` ```vue ``` ### Multiple v-models ```vue ``` ```vue ``` ### v-model with modifiers ```vue ``` ```vue ``` --- ## Slots ### Named slots with TypeScript types ```vue ``` ```vue ``` ### Renderless components ```vue ``` ```vue Cursor: {{ x }}, {{ y }} ``` ### useSlots() in composables ```ts import { useSlots, computed } from 'vue' // Check if a named slot is provided export function useHasSlot(name: string) { const slots = useSlots() return computed(() => !!slots[name]) } ``` --- ## Transitions ### CSS transitions ```vue ``` ### JavaScript hooks (GSAP / Web Animations API) ```vue ``` ### TransitionGroup — list animations ```vue ``` --- ## Teleport ### Modal pattern ```vue ``` ### Disabling Teleport conditionally ```vue
Content
``` --- ## Suspense ### Async setup with Suspense ```vue ``` ```vue ``` ### Error handling with Suspense ```vue ``` --- ## Custom Directives ### vFocus — auto-focus on mount ```ts // directives/vFocus.ts import type { Directive } from 'vue' export const vFocus: Directive = { mounted(el) { el.focus() } } ``` ```vue ``` ### vClickOutside — dismiss on outside click ```ts // directives/vClickOutside.ts import type { Directive } from 'vue' type ClickOutsideHandler = (event: MouseEvent) => void export const vClickOutside: Directive = { mounted(el, binding) { el._clickOutside = (event: MouseEvent) => { if (!el.contains(event.target as Node)) { binding.value(event) } } document.addEventListener('click', el._clickOutside) }, unmounted(el) { document.removeEventListener('click', el._clickOutside) delete el._clickOutside }, } ``` ### vIntersect — visibility tracking ```ts // directives/vIntersect.ts import type { Directive } from 'vue' interface IntersectBinding { handler: (isIntersecting: boolean) => void options?: IntersectionObserverInit } export const vIntersect: Directive = { mounted(el, { value }) { const observer = new IntersectionObserver( ([entry]) => value.handler(entry.isIntersecting), value.options ) observer.observe(el) el._intersectObserver = observer }, unmounted(el) { el._intersectObserver?.disconnect() }, } ``` ### Registering directives globally ```ts // main.ts import { createApp } from 'vue' import { vFocus } from '@/directives/vFocus' import { vClickOutside } from '@/directives/vClickOutside' const app = createApp(App) app.directive('focus', vFocus) app.directive('click-outside', vClickOutside) app.mount('#app') ``` ### Directive lifecycle hooks reference ```ts const myDirective: Directive = { created(el, binding, vnode) {}, // before component attrs/events applied beforeMount(el, binding, vnode) {}, // before element inserted into DOM mounted(el, binding, vnode) {}, // after element inserted, children mounted beforeUpdate(el, binding, vnode, prevVnode) {}, // before parent component updates updated(el, binding, vnode, prevVnode) {}, // after parent and children updated beforeUnmount(el, binding, vnode) {}, // before element removed unmounted(el, binding, vnode) {}, // after element removed } // binding object shape: // binding.value — value passed to directive (v-my-dir="value") // binding.oldValue — previous value (updated hook only) // binding.arg — argument (v-my-dir:arg) // binding.modifiers — object { lazy: true } for v-my-dir.lazy // binding.instance — component instance ```