From Options to Composition: Why Vue 3’s Composition API Is a Smart Engineering Decision
When Vue 3’s Composition API arrives, teams often hesitate. This blog shows a real counter component refactor, TypeScript benefits, unit‑testing ease, and the trade‑offs to consider before making the switch.
11 Aug 2025, 22:34 UTC

Concrete Problem: Inherited Boilerplate and Hard‑to‑Test Logic
In a growing Vue 3 codebase many components still use the classic data, methods, and props options. When a feature is needed in several places, developers either duplicate code or create mixins. Mixins quickly become tangled, and the component’s setup‑phase is missing, making unit tests feel like black‑box experiments.
Thesis: The Composition API Gives You Reusable, Type‑Safe, Testable Logic
Switching to the Composition API turns component logic into small, importable functions—composables. These functions hold reactive state, expose helpers, and can be unit‑tested in isolation. The result is cleaner components, less boilerplate, and a smoother TypeScript experience.
Section 1: Rewriting a Simple Counter
Below is a classic Options API counter component:
export default {
data() { return { count: 0 } },
methods: {
increment() { this.count++ }
}
}
Refactor it to a composable:
// useCounter.ts
import { ref } from 'vue'
export function useCounter(initial = 0) {
const count = ref(initial)
const increment = () => { count.value++ }
return { count, increment }
}
And the component:
import { defineComponent } from 'vue'
import { useCounter } from '@/composables/useCounter'
export default defineComponent({
setup() {
const { count, increment } = useCounter()
return { count, increment }
}
})
Notice the elimination of the data and methods options. The component now only wires the composable into its template.
Section 2: TypeScript Gains & IDE Confidence
Because useCounter returns typed refs, IDEs can infer the type of count automatically. The compiler warns if increment is called incorrectly, catching errors before runtime.
Example of a misuse that TypeScript flags:
const { count } = useCounter()
count = 5 // ❌ Type "number" is not assignable to type "Ref"
Section 3: Unit‑Testing Composables
Testing a composable is straightforward because it is a plain function. Using Jest and Vue Test Utils:
import { useCounter } from '@/composables/useCounter'
import { flushPromises } from '@vue/test-utils'
test('increment increases count', async () => {
const { count, increment } = useCounter(10)
increment()
await flushPromises()
expect(count.value).toBe(11)
})
Compare this to testing an Options API component, where you must mount the component and query the DOM or component instance to verify state changes.
Trade‑Off: Learning Curve & Over‑Abstraction
Teams used to the Options API may find the hook‑style syntax unfamiliar. Introducing composables too early can lead to a fragmented codebase if developers create ad‑hoc composables without a naming convention.
Mitigation: Start with a small, shared composable (e.g., useCounter), document its API, and enforce a src/composables folder structure. Gradually replace legacy options where the benefit is clear.
Actionable Take‑Aways
- Identify a component that repeats logic across the app and extract that logic into a composable.
- Run Vue Devtools before and after refactoring to confirm that reactive state remains exposed.
- Add a Jest test for the new composable and verify passing results.
- Use the Vue CLI bundle analyzer to ensure the refactor does not inflate the bundle size.
By following these steps, you’ll gradually shift your codebase toward a cleaner, more maintainable architecture without sacrificing existing functionality.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.