Vue 3 Composition API: A Complete Guide to setup and composables

Vue 3 ships two component authoring styles: Options API and Composition API. The officially recommended modern approach is "Composition API + <script setup> + Single-File Components (SFC)" — it groups code by logical concern and supports logic reuse naturally. For website teams, Vue 3 works both as a lightweight tool for progressively enhancing static pages and as a full stack for SPA and SSR applications.

Vue content lives under the frontend building category on 16IDC; for a framework comparison, see the frontend framework comparison.

Take one real refactor: an admin page with 40+ form fields and cross-checking logic across three APIs. Written in the Options API, its methods held over 300 lines of mutually referencing functions, and a one-line change often meant hunting through several blocks. After migrating to the Composition API, the validation logic collapsed into three composables — state, requests, and error handling each in their own place — cutting the code by nearly half while making it easier for teammates to pick up. This kind of "logic-dense" component is exactly where the Composition API pays off most.

Two API Styles

The Options API organizes code with options such as data, methods, and mounted, centered on the this component instance — friendlier for beginners. The Composition API writes logic as plain functions used directly in <script setup>, giving freer code organization and better reuse and type inference. Both styles share the same reactivity core — the Options API is implemented on top of the Composition API.

<script setup>: Less Boilerplate

<script setup> is compile-time sugar for SFCs: imported components, top-level variables, and functions are automatically exposed to the template — no setup() return value and no export default needed.

<script setup>
import { ref } from 'vue'

const count = ref(0)
function increment() {
  count.value++
}
</script>

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

Reactive State, computed and watch

ref wraps primitives in reactive references; reactive handles objects; computed derives data; watch/watchEffect observe changes. The Composition API lets you declare these states directly in function scope and compose functions to handle complexity.

In practice, the division of labor between computed and watch is worth spelling out: computed is for "deriving a new value from existing state" — a cart total, a filtered list — and it is cached, recomputing only when its dependencies change; watch is for "running side effects after state changes" — debounced requests on search input, resetting a form on route change. If you are merely displaying a derived value, prefer computed and keep the template declarative; reach for watch only when you genuinely need to "do something" on change. The rule of thumb: anything that can be computed directly in the template does not belong in a watch.

Composables: Reuse and Organization

A composable is a function that encapsulates and reuses stateful logic via the Composition API, by convention named with a use prefix. Mouse tracking, API requests, and form validation can all be extracted into composables, which can also nest and compose with one another. Compared with Vue 2 mixins, composables solve three problems — unclear property sources, namespace collisions, and implicit cross-mixin coupling — making them the recommended reuse mechanism in Vue 3.

// useFetch.js
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  watchEffect(() => {
    data.value = null
    error.value = null
    fetch(toValue(url))
      .then((res) => res.json())
      .then((json) => (data.value = json))
      .catch((err) => (error.value = err))
  })
  return { data, error }
}

The useFetch above accepts a string, a ref, or a getter; toValue normalizes the input and watchEffect re-fetches whenever a tracked dependency changes — loading, success, and error states are all converged in one place.

Migrating from the Options API

You do not have to migrate in one step — migrate component by component. Here is a handy mapping table:

Options API Composition API
data() ref() / reactive()
computed computed()
methods Plain functions
watch watch() / watchEffect()
mounted / unmounted onMounted() / onUnmounted()
mixins Composables

A practical tip: migrate the "logic-heavy" components first (forms, lists, data fetching); leaving purely presentational components in the Options API is perfectly fine — do not force a rewrite just for consistency. You can also mix <script setup> with the Options API during the transition, then gradually move data/methods into the composition style.

Best Practices

  • Naming: camelCase, prefixed with use
  • Return values: return a plain object containing multiple refs so destructuring keeps reactivity
  • Side effects: in SSR, run DOM side effects in onMounted and clean them up in onUnmounted
  • Call sites: only call composables synchronously in <script setup> or setup()

A reusable rule of thumb: once you wrap "state + request + cleanup" into a composable, the component is left with only declarative calls, e.g. const { data, error, retry } = useFetch('/api/user'). Even when the API list or parameters change, the edit stays in a single file — teammates no longer have to dig through every component to understand where the data comes from, and readability and maintainability both improve noticeably.

Engineering Adoption

The official Vue 3 toolchain includes Vite (see the Vite build tool guide), Vue Router, and Pinia. With TypeScript, <script setup> yields strong type inference (see the TypeScript guide). For styling you can use Tailwind (see the Tailwind page template) or reference the HTML page template.

Common questions

Should I convert the whole codebase to <script setup>? Not necessarily. Vue officially supports both styles; use the Composition API for new components and migrate existing ones in batches by business value.

ref or reactive? The community default is to prefer ref (with toRefs/computed it covers most cases); reach for reactive when deep object structures get unwieldy. ref also makes destructuring and argument passing simpler to reason about.

Can a composable use another composable? Yes. As long as they are called synchronously inside <script setup> or setup(), composables nest freely — one of the key differences from mixins.

Source: https://vuejs.org/guide/introduction.html
Reference: Vue 3 Composition API FAQ https://vuejs.org/guide/extras/composition-api-faq.html