Jul 5, 2026
Practical observations from switching from the Options API (Vue 2 and early Vue 3) to the Composition API — what maps across, what doesn't, and what's genuinely better.
I've been using the Options API since around 2018, starting with Vue 2 and carrying the same style into early Vue 3 projects. Most of what I've built with it has been moderate-complexity admin interfaces and data-driven dashboards — the kind of thing where Vue's reactivity model is a good fit and the Options API's structure keeps things organised.
Even after Vue 3 landed, I stuck with the Options API for a while. The Composition API looked like more ceremony for the same result. After finally going deep on it for a real project, I was wrong about that — but my initial reaction wasn't entirely irrational either.
The mental model for reactivity doesn't change. ref() behaves like a reactive data property — it's a container that Vue watches for changes and re-renders when updated. reactive() is closer to the whole data object, but in practice ref() is more useful because you can pass it around as a standalone value.
computed() is a direct analogue to Options API computed. watch() and watchEffect() cover what watch and immediate watchers covered before.
onMounted, onUnmounted, onBeforeMount — all there, just imported as functions instead of being methods on the component options object. The behaviour is the same.
defineProps and defineEmitsThese replace the props and emits options. The TypeScript-first versions (defineProps<{ title: string }>()) are cleaner than the object syntax, though the type inference quirks take some getting used to.
This is the big one. In the Options API, sharing logic across components meant mixins — which worked but had well-documented problems (namespace collisions, unclear provenance, hard to type with TypeScript).
Composables are just functions that use Vue's composition API internally. They can hold reactive state, register lifecycle hooks, and return whatever the caller needs. The caller's template and component logic are cleaner because the data-fetching, the loading state, and the error handling all live in a function with a clear name.
export function useContent(type: string, slug: string) {
const data = ref(null)
const loading = ref(true)
const error = ref<string | null>(null)
onMounted(async () => {
try {
const response = await fetch(`/api/content/${type}/${slug}`)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
data.value = await response.json()
} catch (e) {
error.value = e instanceof Error ? e.message : 'Unknown error'
} finally {
loading.value = false
}
})
return { data, loading, error }
}The component that uses this doesn't care how the data is fetched. It just calls the composable and uses the returned refs.
Related logic lives together. In the Options API, a feature that needed data, a computed property, a watcher, and a method spread those across four different sections of the component options. In <script setup>, they sit next to each other.
This matters more as components grow. A component with three loosely related features reads much more clearly when each feature's logic is grouped together rather than split by type.
<script setup> and Template RefsTemplate refs in <script setup> require declaring a ref with the same name and the right type: const myEl = ref<HTMLElement | null>(null). This is the right approach but it's not obvious until you've seen it once.
The generic syntax for defineProps is elegant, but optional props with defaults require withDefaults(), and the interaction between withDefaults and complex prop types has sharp edges. The compiler macros are technically not standard TypeScript, which can confuse tools occasionally.
Destructuring a reactive() object loses reactivity — a fact that has bitten me more than once. The rule "use ref() for primitives, avoid destructuring reactive()" is simple, but it's easy to forget when you're moving fast.
The Composition API is better, but not because it's simpler. It's better because the patterns it enables — composables, co-located logic, clean TypeScript integration — produce more maintainable code at scale. If you're building small components, the Options API is arguably less noisy. If you're building a real application, the Composition API pays off.
The migration cost for an existing Options API codebase is real. For a new project, there's no reason not to use Composition API from the start.