Vue 3 Composition API Patterns I Use Every Day
Practical composable patterns — async state management, form handling, and reusable logic that keeps Vue 3 codebases clean without premature abstraction.
Mokammel Tanvir
Software Engineer
Why I Left the Options API
The Options API isn't bad. For small components it's perfectly readable. But when a component grows past a hundred lines, you feel it — logic that belongs together gets split across data, computed, methods, and multiple lifecycle hooks.
Composition API organizes code by feature, not by hook type. Once I switched, I didn't go back.
Async State: One Pattern for Everything
Every async operation in my Vue components gets wrapped in this composable. Loading state, error state, and the execute function — all in one place, consistent across the entire app.
// app/composables/useAsync.ts
export function useAsync<T>(fn: () => Promise<T>) {
const data = ref<T | null>(null)
const loading = ref(false)
const error = ref<string | null>(null)
async function execute(): Promise<void> {
loading.value = true
error.value = null
try {
data.value = await fn()
} catch (e: unknown) {
error.value = e instanceof Error ? e.message : 'Something went wrong'
} finally {
loading.value = false
}
}
return { data: readonly(data), loading: readonly(loading), error: readonly(error), execute }
}
In a component:
<script setup lang="ts">
const { data: projects, loading, execute: load } = useAsync(
() => $fetch('/api/v1/projects')
)
onMounted(load)
</script>
<template>
<div v-if="loading">Loading...</div>
<ProjectList v-else :projects="projects ?? []" />
</template>
Form State That Handles Validation
For forms I want reactive field values, server-side validation errors mapped to fields, and a clean reset function.
// app/composables/useForm.ts
export function useForm<T extends Record<string, any>>(initial: T) {
const form = reactive({ ...initial })
const errors = reactive<Partial<Record<keyof T, string>>>({})
function reset() {
Object.assign(form, initial)
Object.keys(errors).forEach(k => delete errors[k as keyof T])
}
function setErrors(serverErrors: Record<string, string[]>) {
Object.keys(serverErrors).forEach(key => {
(errors as any)[key] = serverErrors[key][0] // take first message per field
})
}
return { form, errors, reset, setErrors }
}
<script setup lang="ts">
const { form, errors, reset, setErrors } = useForm({ email: '', password: '' })
async function submit() {
try {
await $fetch('/api/login', { method: 'POST', body: form })
} catch (e: any) {
setErrors(e.data?.errors ?? {})
}
}
</script>
<template>
<input v-model="form.email" />
<span v-if="errors.email" class="text-red-500">{{ errors.email }}</span>
</template>
The Naming Rule
Each composable should do one thing. The name should say what that thing is — useAuth, useForm, useActiveSection, useAsync.
If I'm struggling to name a composable, it's doing too much. That's usually the signal to break it apart.
The Composition API's real advantage isn't just reuse — it's being able to read a component and know exactly which external logic it depends on, by name, without hunting through a mixed bag of options.

Mokammel Tanvir
Full-Stack Engineer · Laravel · Vue · WordPress · AI
Building web applications with Laravel, Vue/Nuxt, and WordPress — SaaS platforms, REST APIs, and AI-integrated workflows. Open to remote and hybrid opportunities.