--- name: vue-composables description: Write high-quality Vue 3 composables following established patterns and best practices. Use when creating new composables, refactoring existing ones, or reviewing composable code. Triggers include requests to "create a composable", "write a use* function", "extract logic into a composable", or any Vue Composition API reusable logic task. --- # Vue 3 Composables Style Guide ## Naming Conventions ### Files - Prefix with `use` and use PascalCase: `useCounter.ts`, `useApiRequest.ts` - Place in `src/composables/` directory ### Functions - Use descriptive names: `useUserData()` not `useData()` - Export as named function: `export function useCounter() {}` ## Structure Template Follow this order consistently: ```ts import { computed, onMounted, ref, watch } from 'vue' export function useExample() { // 1. Initializing - setup logic, router, external dependencies // 2. Primary State - main reactive state const data = ref(null) // 3. State Metadata - status, errors, loading const status = ref<'idle' | 'loading' | 'success' | 'error'>('idle') const error = ref(null) // 4. Computed - derived state const isLoading = computed(() => status.value === 'loading') // 5. Methods - state manipulation const fetchData = async () => { status.value = 'loading' try { // fetch logic status.value = 'success' } catch (e) { status.value = 'error' error.value = e instanceof Error ? e : new Error(String(e)) } } // 6. Lifecycle Hooks onMounted(() => { // initialization logic }) // 7. Watchers watch(data, (newValue) => { // react to changes }) return { data, status, error, isLoading, fetchData } } ``` ## Core Rules ### Single Responsibility One composable = one purpose. Avoid mixing unrelated concerns. ```ts // GOOD - focused on one task export function useCounter() { const count = ref(0) const increment = () => { count.value++ } const decrement = () => { count.value-- } return { count, increment, decrement } } // BAD - mixing user data and counter export function useUserAndCounter() { const user = ref(null) const count = ref(0) // ... mixed concerns } ``` ### Expose Error State Return errors for component handling. Never swallow errors or show UI directly. ```ts // GOOD const error = ref(null) try { await fetchData() } catch (e) { error.value = e instanceof Error ? e : new Error(String(e)) } return { error } // BAD - swallowing errors or showing UI try { await fetchData() } catch (e) { console.error(e) // swallowed showToast('Error!') // UI in composable } ``` ### No UI Logic in Composables Keep composables focused on state/logic. Handle UI in components. ```ts // GOOD - composable returns state export function useUserData(userId: string) { const user = ref(null) const error = ref(null) const fetchUser = async () => { /* ... */ } return { user, error, fetchUser } } // Component handles UI const { error } = useUserData(userId) watch(error, (e) => { if (e) showToast('Error occurred') }) ``` ### Object Arguments for 4+ Parameters ```ts // GOOD - object for many params useUserData({ id: 1, fetchOnMount: true, token: 'abc', locale: 'en' }) // GOOD - positional for few params useCounter(initialValue, step) // BAD - too many positional args useUserData(1, true, 'abc', 'en', false, 'default') ``` ### Group Related State into Objects When a composable has 4+ related state properties, group them into a single `ref` object instead of separate refs. ```ts // GOOD - grouped state for 4+ related properties interface FormState { name: string email: string phone: string address: string } export function useContactForm() { const form = ref({ name: '', email: '', phone: '', address: '', }) function reset() { form.value = { name: '', email: '', phone: '', address: '' } } return { form, reset } } // GOOD - separate refs for 1-3 unrelated properties export function useToggle() { const isOpen = ref(false) const isLoading = ref(false) return { isOpen, isLoading } } // BAD - many separate refs for related state export function useContactForm() { const name = ref('') const email = ref('') const phone = ref('') const address = ref('') // ... becomes unwieldy to manage and reset } ``` ### Functional Core, Imperative Shell Extract pure logic from Vue reactivity when beneficial. ```ts // Pure function (testable, no side effects) function calculateTotal(items: ReadonlyArray) { return items.reduce((sum, item) => sum + item.price, 0) } // Composable uses the pure function export function useCart() { const items = ref>([]) const total = computed(() => calculateTotal(items.value)) return { items, total } } ``` ## Quick Reference | Aspect | Do | Don't | |--------|-----|-------| | Naming | `useUserData`, `useFetchApi` | `useData`, `getData` | | File | `useCounter.ts` in `composables/` | `counter.ts` anywhere | | Errors | Return `error` ref | `console.error()` or toast | | UI | Return state, handle UI in component | `showModal()` in composable | | Params | Object for 4+ params | Long positional arg lists | | State | `ref` object for 4+ related properties | Many separate refs | | Focus | Single responsibility | Mixed concerns |