Vue Composition API Introduction

The Composition API is Vue 3's function-based way of writing components, letting you group related logic together instead of splitting it across data, methods, and computed.

Why a New API?

In the Options API, a single feature -- say, a search box with debouncing -- often has its state in data, its logic in methods, and its derived values in computed, spread across the file in whatever order those options happen to be declared. As a component grows, related code drifts apart. The Composition API instead lets you write all the reactive state and logic for one feature together as plain JavaScript, in whatever grouping makes sense, and extract it into a reusable function -- a composable -- when another component needs the same behavior.

setup() and <script setup>

The Composition API's entry point is a setup() function that runs once, before the component is mounted, and returns the values and functions the template should have access to. <script setup> is compiler-time sugar for this: everything you declare at the top level of a <script setup> block -- variables, functions, imports -- is automatically exposed to the template, so you never write an explicit return statement.

A Component Written with <script setup>

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

const count = ref(0)
const doubled = computed(() => count.value * 2)

function increment() {
  count.value++
}
</script>

<template>
  <p>Count: {{ count }} (doubled: {{ doubled }})</p>
  <button @click='increment'>Add one</button>
</template>

ref vs reactive

ref() wraps any value -- a number, a string, an object, an array -- in a reactive container with a single .value property; you read and write that property in script code, but Vue automatically unwraps it in the template, so you write count rather than count.value there. reactive() instead wraps an object directly, making its properties reactive without a .value indirection, but only for objects: reactive(0) doesn't work, and reassigning the whole object with = replaces the reactive proxy, breaking the connection. As a rule of thumb, use ref for standalone values and reactive for grouped, related state like a form.

Choosing Between ref and reactive

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

// A single value: ref
const submitCount = ref(0)

// A group of related fields: reactive
const form = reactive({
  email: '',
  password: ''
})

function submit() {
  submitCount.value++
  console.log('Submitting', form.email)
}
</script>

<template>
  <input v-model='form.email' placeholder='Email' />
  <input v-model='form.password' type='password' placeholder='Password' />
  <button @click='submit'>Log in ({{ submitCount }})</button>
</template>

The Same Component, the Options Way

Seeing the two styles side by side makes the trade-off concrete. Below is a counter component written with the Options API -- notice how count, doubled, and increment are declared in three separate options, even though they describe one small piece of behavior.

The Options API Equivalent

<script>
export default {
  data() {
    return { count: 0 }
  },
  computed: {
    doubled() {
      return this.count * 2
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>

<template>
  <p>Count: {{ count }} (doubled: {{ doubled }})</p>
  <button @click='increment'>Add one</button>
</template>

Mapping Concepts Between the Two APIs

Options APIComposition APINotes
data()ref() / reactive()Composition API state is created by calling functions, not returning an object
computed: { ... }computed(() => ...)Same caching behavior, just written as a function call
methods: { ... }Plain functions declared in setupNo this -- read reactive values directly
mounted() etc.onMounted() etc.Imported from vue and called inside setup
this.countcount.value in script, count in templaterefs need .value everywhere except the template
Note: You can adopt the Composition API one component at a time -- Vue 3 supports both styles in the same project indefinitely, so there's no need to rewrite an entire app just to try it.
Note: Never destructure a reactive() object directly, like pulling email straight out of form -- that copies the current value out and disconnects it from reactivity. If you need standalone reactive variables from an object, use toRefs(form) first.

Exercise: Vue Composition API

What is a key difference between ref() and reactive() for primitive values?