| 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:
import { computed, onMounted, ref, watch } from 'vue'
export function useExample() {
const data = ref<Data | null>(null)
const status = ref<'idle' | 'loading' | 'success' | 'error'>('idle')
const error = ref<Error | null>(null)
const isLoading = computed(() => status.value === 'loading')
const fetchData = async () => {
status.value = 'loading'
try {
status.value = 'success'
}
catch (e) {
status.value = 'error'
error.value = e instanceof Error ? e : new Error(String(e))
}
}
onMounted(() => {
})
watch(data, (newValue) => {
})
return { data, status, error, isLoading, fetchData }
}
Core Rules
Single Responsibility
One composable = one purpose. Avoid mixing unrelated concerns.
export function useCounter() {
const count = ref(0)
const increment = () => {
count.value++
}
const decrement = () => {
count.value--
}
return { count, increment, decrement }
}
export function useUserAndCounter() {
const user = ref(null)
const count = ref(0)
}
Expose Error State
Return errors for component handling. Never swallow errors or show UI directly.
const error = ref<Error | null>(null)
try {
await fetchData()
}
catch (e) {
error.value = e instanceof Error ? e : new Error(String(e))
}
return { error }
try {
await fetchData()
}
catch (e) {
console.error(e)
showToast('Error!')
}
No UI Logic in Composables
Keep composables focused on state/logic. Handle UI in components.
export function useUserData(userId: string) {
const user = ref<User | null>(null)
const error = ref<Error | null>(null)
const fetchUser = async () => { }
return { user, error, fetchUser }
}
const { error } = useUserData(userId)
watch(error, (e) => {
if (e)
showToast('Error occurred')
})
Object Arguments for 4+ Parameters
useUserData({ id: 1, fetchOnMount: true, token: 'abc', locale: 'en' })
useCounter(initialValue, step)
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.
interface FormState {
name: string
email: string
phone: string
address: string
}
export function useContactForm() {
const form = ref<FormState>({
name: '',
email: '',
phone: '',
address: '',
})
function reset() {
form.value = { name: '', email: '', phone: '', address: '' }
}
return { form, reset }
}
export function useToggle() {
const isOpen = ref(false)
const isLoading = ref(false)
return { isOpen, isLoading }
}
export function useContactForm() {
const name = ref('')
const email = ref('')
const phone = ref('')
const address = ref('')
}
Functional Core, Imperative Shell
Extract pure logic from Vue reactivity when beneficial.
function calculateTotal(items: ReadonlyArray<Item>) {
return items.reduce((sum, item) => sum + item.price, 0)
}
export function useCart() {
const items = ref<Array<Item>>([])
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 |