---
title: "Vue 3 and the Composition API, two years in."
date: 2022-03-14
url: https://remiam.co.uk/notes/vue-3-composition-api-in-production
tags: [Vue, Composition API, Patterns]
read_time_minutes: 10
description: "Two years of running the Vue 3 Composition API in production — what worked, what hurt, the patterns we'd reach for again, with code examples and migration guidance."
---

# Vue 3 and the Composition API, two years in.

*Published 2022-03-14 · 10 min read · by Liam (Remiam)*

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

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

## What hurt

- Reactivity edge cases — destructured props, .value forgetfulness, accidental loss of reactivity in async paths.
- Pinia is great but it doesn't replace good composable boundaries.
- Junior engineers needed about two weeks to be productive in Composition. Worth it, but plan for it.
- The migration path from Vue 2 was non-trivial — large codebases needed serious refactor time.
- Some Options API patterns don't translate cleanly. Provide / inject across deep trees is awkwarder than it should be.
- Older 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.

```typescript composables/useFetchOnFocus.ts
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 (typeof window !== 'undefined') {
    window.addEventListener('focus', onFocus)
    onBeforeUnmount(() => window.removeEventListener('focus', onFocus))
  }

  return { data, error, loading, refresh }
}
```

## And how the consuming component looks

```vue pages/dashboard.vue (script setup)
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

- Don't migrate the whole codebase at once. Component-by-component as you touch them.
- Use the Vue 2.7 'composition API compat' release as a bridge — write Composition API code in Vue 2 first, then upgrade to Vue 3.
- Move shared state into composables before changing components. The composables work in both APIs.
- Replace Vuex with Pinia in the same PR as the framework upgrade — both work the same, the upgrade is mostly imports.
- Audit your third-party Vue plugins. Anything not updated for Vue 3 in the last year is a migration blocker.
- Budget 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.

## References

1. [Vue 3 — Composition API documentation](https://vuejs.org/guide/extras/composition-api-faq.html)
2. [Pinia — state management for Vue 3](https://pinia.vuejs.org/)
3. [VueUse — collection of essential Vue composables](https://vueuse.org/)
