# Vue Composables Reusable functions encapsulating stateful logic using Composition API. ## Core Rules 1. **VueUse first** - check [vueuse.org](https://vueuse.org) before writing custom 2. **No async composables** - lose lifecycle context when awaited in other composables 3. **Top-level only** - never call in event handlers, conditionals, or loops 4. **readonly() exports** - protect internal state from external mutation 5. **useState() for SSR** - use Nuxt's `useState()` not global refs ## Quick Reference | Pattern | Example | | --------- | ------------------------------------------------ | | Naming | `useAuth`, `useCounter`, `useDebounce` | | State | `const count = ref(0)` | | Computed | `const double = computed(() => count.value * 2)` | | Lifecycle | `onMounted(() => ...)`, `onUnmounted(() => ...)` | | Return | `return { count, increment }` | ## Structure ```ts // composables/useCounter.ts import { readonly, ref } from 'vue' export function useCounter(initialValue = 0) { const count = ref(initialValue) function increment() { count.value++ } function decrement() { count.value-- } function reset() { count.value = initialValue } return { count: readonly(count), // readonly if shouldn't be mutated increment, decrement, reset, } } ``` ## Naming **Always prefix with `use`:** `useAuth`, `useLocalStorage`, `useDebounce` **File = function:** `useAuth.ts` exports `useAuth` ## Best Practices **Do:** - Return object with named properties (destructuring-friendly) - Accept options object for configuration - Use `readonly()` for state that shouldn't mutate - Handle cleanup (`onUnmounted`, `onScopeDispose`) - Add JSDoc for complex functions ## Lifecycle Hooks execute in component context: ```ts export function useEventListener(target: EventTarget, event: string, handler: Function) { onMounted(() => target.addEventListener(event, handler)) onUnmounted(() => target.removeEventListener(event, handler)) } ``` **Watcher cleanup (Vue 3.5+):** ```ts import { watch, onWatcherCleanup } from 'vue' export function usePolling(url: Ref) { watch(url, (newUrl) => { const interval = setInterval(() => { fetch(newUrl).then(/* ... */) }, 1000) // Cleanup when watcher re-runs or stops onWatcherCleanup(() => { clearInterval(interval) }) }) } ``` **Benefits of `onWatcherCleanup()`:** - Cleaner than returning cleanup functions - Works with async watchers - Can be called multiple times in same watcher ## Async Pattern ```ts export function useAsyncData(fetcher: () => Promise) { const data = ref(null) const error = ref(null) const loading = ref(false) async function execute() { loading.value = true error.value = null try { data.value = await fetcher() } catch (e) { error.value = e as Error } finally { loading.value = false } } execute() return { data, error, loading, refetch: execute } } ``` **Data fetching:** Prefer Pinia Colada queries over custom composables. ## VueUse > For VueUse composable reference, use the `vueuse` skill. Check VueUse before writing custom composables - most patterns already implemented. > **For Nuxt-specific composables** (useFetch, useRequestURL): see `nuxt` skill nuxt-composables.md ## Advanced Patterns ### Singleton Composable Share state across all components using the same composable: ```ts import { createSharedComposable } from '@vueuse/core' function useMapControlsBase() { const mapInstance = ref(null) const flyTo = (coords: [number, number]) => mapInstance.value?.flyTo(coords) return { mapInstance, flyTo } } export const useMapControls = createSharedComposable(useMapControlsBase) ``` ### Cancellable Fetch with AbortController ```ts export function useSearch() { let abortController: AbortController | null = null watch(query, async (newQuery) => { abortController?.abort() abortController = new AbortController() try { const data = await $fetch('/api/search', { query: { q: newQuery }, signal: abortController.signal, }) } catch (e) { if (e.name !== 'AbortError') throw e } }) } ``` ### Step-Based State Machine ```ts export function useSendFlow() { const step = ref<'input' | 'confirm' | 'success'>('input') const amount = ref('') const next = () => { if (step.value === 'input') step.value = 'confirm' else if (step.value === 'confirm') step.value = 'success' } return { step, amount, next } } ``` ### Client-Only Guards ```ts export function useUserLocation() { const location = ref(null) if (import.meta.client) { navigator.geolocation.getCurrentPosition(pos => location.value = pos) } return { location } } ``` ### Custom Element Composables (Vue 3.5+) For custom element components, use built-in helpers: ```ts import { useHost, useShadowRoot } from 'vue' export function useCustomElement() { const host = useHost() // Host element reference const shadowRoot = useShadowRoot() // Shadow DOM root onMounted(() => { console.log('Host:', host) console.log('Shadow:', shadowRoot) }) return { host, shadowRoot } } ``` **Available in:** - Components using `