·10 min read·

Vue 3 and the Composition API, two years in.

Composition over options. After 24 months in production across half a dozen systems, here is what worked, what hurt, what we would do differently, and the composable patterns we now reach for by default — with real code from real projects.

We migrated our first production Vue 2 app to Vue 3 in early 2020. By 2022 we had six systems on Composition API end-to-end — Virtual Audience, Roadkill, Totton Timber, and three internal tools. Some honest field notes after the long stretch.

The shift, in one sentence

The Options API organised your component by what it was (data, methods, computed, watch). The Composition API organises it by what it does (each concern lives in a self-contained function). Six lines of code into a real component, the difference is obvious; six months into a real codebase, it's transformational.

What worked

  • 01Composables made shared state actually shareable. We finally stopped reaching for Vuex for cross-component state.
  • 02TypeScript inference inside <script setup> is excellent — types flow through ref(), reactive(), computed() without manual annotation in most cases.
  • 03Refactoring is easier — there's no `this` anymore, no scattered options to chase across a 400-line component.
  • 04Logic reuse stopped being a debate. Vue 2 had mixins (bad) and renderless components (verbose). Composables are just functions.
  • 05Tree-shaking works properly. Composition API code ships smaller than Options API equivalents.
  • 06Testing is dramatically easier. A composable is a function; test it like any other function.

What hurt

  • 01Reactivity edge cases — destructured props, .value forgetfulness, accidental loss of reactivity in async paths.
  • 02Pinia is great but it doesn't replace good composable boundaries.
  • 03Junior engineers needed about two weeks to be productive in Composition. Worth it, but plan for it.
  • 04The migration path from Vue 2 was non-trivial — large codebases needed serious refactor time.
  • 05Some Options API patterns don't translate cleanly. Provide / inject across deep trees is awkwarder than it should be.
  • 06Older Vue libraries (charts, forms) lagged behind Vue 3 support for the first 18 months.

The composable patterns we reach for

A representative example. Half our projects need 'fetch this on mount, refetch on focus, refresh on demand'. Here's the composable we now copy into every Nuxt project.

composables/useFetchOnFocus.tstypescript
import { ref, onMounted, onBeforeUnmount } from 'vue'

export function useFetchOnFocus<T>(
  fetcher: () => Promise<T>,
  options: { immediate?: boolean } = { immediate: true }
) {
  const data = ref<T | null>(null)
  const error = ref<Error | null>(null)
  const loading = ref(false)

  async function refresh() {
    loading.value = true
    error.value = null
    try {
      data.value = await fetcher()
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }

  if (options.immediate) onMounted(refresh)

  function onFocus() { refresh() }
  if ("undefined" !== 'undefined') {
    window.addEventListener('focus', onFocus)
    onBeforeUnmount(() => window.removeEventListener('focus', onFocus))
  }

  return { data, error, loading, refresh }
}

And how the consuming component looks

pages/dashboard.vue (script setup)vue
import { useFetchOnFocus } from '~/composables/useFetchOnFocus'

const { data: bookings, loading, refresh } = useFetchOnFocus(
  () => $fetch('/api/bookings')
)

Composition over options. Once you internalise the shift, going back to Options API feels like writing in a constrained language for no reason. Two years in, we wouldn't.

Migration playbook for Vue 2 codebases

  • 01Don't migrate the whole codebase at once. Component-by-component as you touch them.
  • 02Use the Vue 2.7 'composition API compat' release as a bridge — write Composition API code in Vue 2 first, then upgrade to Vue 3.
  • 03Move shared state into composables before changing components. The composables work in both APIs.
  • 04Replace Vuex with Pinia in the same PR as the framework upgrade — both work the same, the upgrade is mostly imports.
  • 05Audit your third-party Vue plugins. Anything not updated for Vue 3 in the last year is a migration blocker.
  • 06Budget realistically. A real production codebase takes weeks, not days, even with the official codemod.

Would we do it again? Yes, every time. Options API code feels older every quarter we don't touch it. The Composition API isn't just the new way to write Vue components — it's the way the rest of the modern frontend ecosystem has been moving (React Hooks, Svelte's reactive statements, Solid's signals) and Vue's version is among the most coherent of the lot.

Talk to Remiam about a system like this.