vue-composables
Vue 3 Composables Style Guide
Naming Conventions
Files
- Prefix with
useand use PascalCase:useCounter.ts,useApiRequest.ts - Place in
src/composables/directory
Functions
- Use descriptive names:
useUserData()notuseData() - Export as named function:
export function useCounter() {}
Structure Template
Follow this order consistently:
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<Data | null>(null)
// 3. State Metadata - status, errors, loading
const status = ref<'idle' | 'loading' | 'success' | 'error'>('idle')
const error = ref<Error | null>(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.
// 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.
// GOOD
const error = ref<Error | null>(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.
// GOOD - composable returns state
export function useUserData(userId: string) {
const user = ref<User | null>(null)
const error = ref<Error | null>(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
// 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.
// GOOD - grouped state for 4+ related properties
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 }
}
// 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.
// Pure function (testable, no side effects)
function calculateTotal(items: ReadonlyArray<Item>) {
return items.reduce((sum, item) => sum + item.price, 0)
}
// Composable uses the pure function
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 |
More from alexanderop/workouttracker
product-planning
|
19vitest-mocking
|
15add-exercises
Add new exercises to the workout tracker database. Use when asked to add exercises, expand the exercise library, or check what exercises exist. Triggers include "add exercise", "new exercise", "exercise database", "what exercises", "missing exercises", "expand exercises".
12systematic-debugging
|
12repository-pattern
Create and manage Dexie/IndexedDB repositories with type-safe interfaces, converters, and standardized CRUD operations. Use when (1) adding entity storage, (2) implementing save/load/delete operations, (3) designing database schema and indexes, (4) converting between database (Db*) and domain types, (5) handling database errors or migrations, (6) using existing repositories (SettingsRepository, WorkoutsRepository, TemplatesRepository, CustomExercisesRepository, BenchmarksRepository, ActiveWorkoutRepository). Triggers include "database", "repository", "save data", "fetch from database", "delete from storage", "database schema", "database table", "indexes", "migration", "persist", "convert workout", "converter", "buildPartialUpdate", "mock repository", "database error", "bulk operations", "import/export", or specific repository names.
12improve-skill
Analyze coding agent session transcripts to improve existing skills or create new ones. Use when asked to improve a skill based on a session, or extract a new skill from session history.
11