Vue 3 has been out since last September, and we've had a real dashboard project running on it for the better part of two quarters now. The headline feature, the Composition API, was the part we were most skeptical about going in, since Options API code had served us well for years and the new syntax looked, at first glance, like change for its own sake.
What actually improved
Extracting shared logic into composables replaced a tangle of mixins almost overnight. Mixins in the Options API era had a well-known failure mode: two mixins on the same component could silently collide on a property name, and tracking down which mixin actually set a given piece of state meant reading every mixin the component used, in order, and mentally merging them. Composables sidestep this entirely, since everything a composable returns is explicit at the call site; there's no implicit merging, and renaming a returned value is a normal JavaScript refactor rather than an exercise in mixin archaeology. We've since extracted composables for things like pagination state, debounced search input, and websocket connection handling, and reused every one of them across at least two components without modification.
Type inference through the Composition API is meaningfully better than the Options API ever managed, which matters more each month as our TypeScript usage grows across the codebase. `defineComponent` combined with `ref` and `reactive` gives the editor enough information to autocomplete props and emitted events correctly almost all the time, something that required manual type annotations and was frequently wrong under the Options API's `this`-based context. For a dashboard project with a genuinely complex data model, this alone justified a meaningful chunk of the migration effort.
- Extracting shared logic into composables replaced a tangle of mixins almost overnight, and the naming collisions that plagued our old mixin-heavy code simply stopped happening.
- Type inference through the Composition API is meaningfully better than the Options API ever managed, which compounds as TypeScript usage grows.
- Component logic organized by concern, rather than by Options API section, made large components easier to navigate once the team adjusted to the new reading order.
- Vue 3's Proxy-based reactivity system removed several of the mutation-detection edge cases that used to require `Vue.set` workarounds under Vue 2.
What the migration actually cost
The learning curve for new team members is real, and we underestimated it going in. Options API code reads more linearly for someone unfamiliar with a codebase, since `data`, `computed`, and `methods` are each in their own clearly labeled section; Composition API code is organized by feature rather than by Vue API category, which is a genuine improvement for large components but a real speed bump for a new hire's first week. We've had to invest in onboarding documentation specifically to cover this, including a short internal guide mapping common Options API patterns to their Composition API equivalents.
Reactivity caveats around destructuring reactive objects tripped up more than one pull request before the team fully internalized the rules. Pulling a property off a `reactive()` object with object destructuring breaks its reactivity, since the destructured value is a plain snapshot rather than a live reference, and the fix, using `toRefs()` before destructuring, isn't something anyone guesses correctly the first time they hit it. We added an ESLint rule to catch the most common version of this mistake, which has cut down on the number of "why isn't this updating" bugs making it into code review.
Rollout strategy and what's next
We didn't rewrite the entire dashboard at once. Vue 3 supports the Options API and Composition API side by side in the same component tree, and we used that to our advantage, migrating components opportunistically whenever we touched them for an unrelated feature or bug fix rather than scheduling a dedicated migration sprint. Roughly 60 percent of the dashboard's components are on the Composition API at this point, and the remaining Options API components have shown no urgency to convert, since they're small, stable, and rarely touched.
Testing composables versus testing components
One benefit we didn't fully anticipate going in was how much easier composables are to unit test in isolation compared to the equivalent Options API logic. A composable is, at its core, a plain function that happens to call Vue's reactivity primitives, which means testing our pagination composable is a matter of calling it directly in a test file and asserting on the returned refs, with no need to mount a full component just to exercise logic that has nothing to do with rendering. Under the Options API, the same logic lived inside a component's `methods` and `computed` blocks, and testing it meant mounting the component through a testing library, providing whatever props and stubs it needed, and reasoning about DOM output even when the thing actually under test was a pure data transformation. We've seen our test suite run noticeably faster on modules that have been migrated, partly because pure composable tests don't carry the overhead of a full component mount.
What we noticed about bundle size and runtime performance
Vue 3's rewritten reactivity system, based on ES6 Proxies rather than Vue 2's `Object.defineProperty` getter and setter overrides, isn't something end users notice directly, but it removed a category of workaround our team used to reach for regularly. Under Vue 2, adding a new property to a reactive object after it was created required `Vue.set` for the change to be tracked, since `Object.defineProperty` can only intercept properties that existed at the time it ran; Vue 3's Proxy-based approach tracks property additions and array index changes without any special-cased API, and every `Vue.set` call site in our codebase disappeared once we finished the Options-to-Composition migration on the affected components. On the bundle size side, Vue 3's better tree-shaking meant our production bundle dropped by a small but measurable amount even before we'd migrated a single component, purely from switching the underlying framework version, since unused parts of the framework's API surface no longer ship by default.
Pairing Vue 3 with Vite for local development has also been a quieter but genuinely significant win; cold start and hot module replacement are both faster than the webpack-based tooling we used with Vue 2, which matters more day to day than any single API design decision.
The `<script setup>` syntax we're watching but not yet using everywhere
A newer, more compact way of writing Composition API components using a `<script setup>` block has started circulating, letting a component skip the explicit `setup()` function and `return` statement entirely and just declare its reactive state and functions directly, with the compiler handling the rest. We've tried it on a handful of new components and it genuinely does cut boilerplate further, particularly the return statement that otherwise has to re-export every piece of state a template needs. We haven't rolled it out across the whole dashboard yet, partly because tooling support and team familiarity with the syntax are both still catching up, and partly because we'd rather let the pattern settle a bit further before standardizing on it project-wide, but it's a strong candidate for where we expect Vue 3 components to end up looking in another six months.
Devtools support caught up slower than the API itself
Vue Devtools' Composition API support lagged behind the API itself during our first couple of months on Vue 3, and it was a real source of friction that's easy to forget about once it's fixed. Inspecting a component's state in the browser extension under early Composition API usage sometimes showed raw refs rather than their unwrapped values, which meant a piece of state that looked like `{ value: 42 }` in the inspector actually needed a mental translation step every time, something the Options API's flatter `data` object never required. Devtools updates through the following months closed most of that gap, unwrapping refs automatically and labeling composable-sourced state more clearly, but during the transition we leaned more heavily on `console.log` and temporary debug renders than we'd have liked, purely because the tooling hadn't fully caught up to the API pattern it was inspecting.
A watcher gotcha that cost us a production bug
One reactivity subtlety that didn't show up in any tutorial we read ahead of time was the difference between `watch` and `watchEffect` around initial execution. `watch` only fires on a change by default, while `watchEffect` runs immediately and then again on every dependency change, and a developer used to the Options API's `watch` handlers, which support an `immediate` option but default to off, ported a piece of logic to `watchEffect` assuming the same default and ended up with a duplicate API call firing on component mount that hadn't been there before. It shipped to production for about a day before anyone noticed the extra network call in our monitoring dashboard. We added a short section to our internal composables guide specifically flagging this difference, since it's exactly the kind of subtle default mismatch that reads correctly at a glance but behaves differently in practice.
The onboarding checklist we eventually wrote down
The internal guide we mentioned earlier for mapping Options API patterns to Composition API equivalents grew into a proper onboarding checklist once we noticed new hires kept hitting the same handful of questions in their first week. It walks through where `data`, `computed`, and `methods` map onto `ref`, `computed`, and plain functions inside `setup`, includes the `toRefs` destructuring gotcha and the `watch` versus `watchEffect` default described above, and ends with a short list of composables already available in the codebase so a new hire doesn't reinvent one that already exists for pagination or debounced input. It's saved enough repeated Slack questions that we now treat updating it as part of the definition of done whenever a genuinely new pattern gets introduced to the codebase.
Where the migration decision would have gone differently
If this dashboard had been a smaller, shorter-lived internal tool rather than something we expect to maintain for years, we probably wouldn't have justified the Composition API migration on its own merits; the Options API is still a perfectly reasonable choice for a small, simple component tree, and rewriting working code purely for a cleaner API is rarely worth it in isolation. What tipped the decision for us was the combination of a growing TypeScript investment, a genuine amount of logic that needed sharing across components, and a long expected lifespan for the project, all three of which compound the value of composables and better type inference over time in a way that a short-lived internal tool simply wouldn't experience.
We wouldn't go back to Vue 2 for a new project at this point. The Composition API's main cost is a steeper ramp for newcomers, and that's a cost worth paying for the long-term maintainability gains it's already delivering on a codebase this size.