Split-screen code editor showing Options API on one side and Composition API on the other with Vue 3 syntax
16 min read Vue 3 Development

Vue 3 Composition API vs Options API: When to Use Each (With Examples)

The Composition API is Vue 3's flagship feature. But is it always better than the Options API you already know? Here's a practical guide to choosing the right approach for your components.

When Vue 3 launched, the Composition API was its most significant and controversial addition. Some developers loved the new flexibility. Others worried it would make Vue feel like React. Three years later, the dust has settled, and we have a clear picture of when each API shines.

The good news: Both APIs are fully supported in Vue 3. You can even mix them in the same project. This guide helps you make informed decisions about which to use—and when. If you're planning a full migration, check out our complete Vue 2 to Vue 3 migration roadmap.

The Quick Answer: Which Should You Use?

Use Options API When...

  • Building simple components
  • Your team is new to Vue
  • Migrating from Vue 2 incrementally
  • The component has clear, separate concerns

Use Composition API When...

  • Building complex components
  • You need to share logic between components
  • Using TypeScript
  • Starting a new Vue 3 project

Our Recommendation for New Projects

For new Vue 3 projects, we recommend defaulting to Composition API with <script setup>. It's more concise, has better TypeScript support, and is where the Vue ecosystem is heading. But don't force it—Options API is still valid for simple components or teams learning Vue.

Understanding the Core Difference

The fundamental difference is how you organize your code. Options API organizes by option type (data, methods, computed). Composition API organizes by logical concern.

Same Component, Two Approaches

Let's build a simple counter with a doubled value:

Options API

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

<template>
  <button @click="increment">
    Count: {{ count }} ({{ doubled }})
  </button>
</template>

Composition API

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

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

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

<template>
  <button @click="increment">
    Count: {{ count }} ({{ doubled }})
  </button>
</template>

For simple components like this, both approaches work equally well. The real differences emerge in complex components.

Options API: The Familiar Approach

If you've used Vue 2, you already know the Options API. It organizes components into predefined sections: data, computed, methods, watch, and lifecycle hooks.

Advantages of Options API

Beginner-Friendly Structure

Clear sections make it easy to know where code goes. New developers can contribute faster.

No Import Boilerplate

No need to import ref, computed, etc. Everything works through this.

Vast Ecosystem of Examples

Most tutorials, Stack Overflow answers, and existing codebases use Options API.

Easy Vue 2 Migration

Vue 2 components work in Vue 3 with minimal changes when using Options API.

Limitations of Options API

Logic Fragmentation in Complex Components

Related code gets scattered across data, computed, methods, and watchers. You jump around the file constantly.

Mixins Have Major Drawbacks

The only way to share logic is mixins, which cause naming conflicts and unclear data sources.

Weaker TypeScript Support

this context is hard to type. You need defineComponent and extra workarounds.

Composition API: The Flexible Approach

The Composition API lets you organize code by logical concern rather than option type. Related code stays together. You can extract and reuse logic easily with "composables."

Advantages of Composition API

Better Code Organization

Group related state, computed values, and functions together. No more jumping between sections.

Composables Replace Mixins

Extract logic into reusable functions with clear inputs and outputs. No naming conflicts.

First-Class TypeScript Support

Everything is typed automatically. No this context issues.

Better Tree-Shaking

Only import what you use. Smaller production bundles.

Challenges of Composition API

Learning Curve

Understanding ref vs reactive, .value unwrapping, and reactivity takes time.

More Freedom = More Rope

Without enforced structure, teams need discipline to keep code organized.

Import Overhead

You need to import ref, computed, watch, etc. (Auto-imports can help.)

Where Composition API Really Shines: Complex Components

The true value of Composition API emerges in complex components with multiple concerns. Let's look at a realistic example: a user profile editor with form handling, validation, and API calls.

Options API: The Scattered Code Problem

<script>
export default {
  data() {
    return {
      // User data
      user: null,
      userLoading: false,
      userError: null,
      
      // Form data
      formData: { name: '', email: '' },
      formDirty: false,
      
      // Validation
      errors: {},
      isValid: false
    }
  },
  
  computed: {
    // User computed
    fullName() { return `${this.user?.firstName} ${this.user?.lastName}` },
    
    // Form computed
    hasChanges() { return JSON.stringify(this.formData) !== JSON.stringify(this.originalData) },
    
    // Validation computed
    canSubmit() { return this.isValid && this.hasChanges && !this.userLoading }
  },
  
  methods: {
    // User methods
    async fetchUser() { /* ... */ },
    
    // Form methods
    updateField(field, value) { /* ... */ },
    resetForm() { /* ... */ },
    
    // Validation methods
    validateField(field) { /* ... */ },
    validateAll() { /* ... */ },
    
    // Submit
    async submitForm() { /* ... */ }
  },
  
  watch: {
    // Watch for user changes
    user(newUser) { /* ... */ },
    
    // Watch for form validation
    formData: { deep: true, handler() { /* ... */ } }
  },
  
  mounted() {
    this.fetchUser()
  }
}
</script>

Notice how user logic, form logic, and validation logic are scattered across data, computed, methods, and watch. To understand the form feature, you need to jump to 4 different places.

Composition API: Organized by Feature

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

// ========== User Feature ==========
const user = ref(null)
const userLoading = ref(false)
const userError = ref(null)

const fullName = computed(() => 
  `${user.value?.firstName} ${user.value?.lastName}`
)

async function fetchUser() {
  userLoading.value = true
  try {
    user.value = await api.getUser()
  } catch (e) {
    userError.value = e.message
  } finally {
    userLoading.value = false
  }
}

// ========== Form Feature ==========
const formData = ref({ name: '', email: '' })
const originalData = ref(null)

const hasChanges = computed(() => 
  JSON.stringify(formData.value) !== JSON.stringify(originalData.value)
)

function updateField(field, value) {
  formData.value[field] = value
}

function resetForm() {
  formData.value = { ...originalData.value }
}

// ========== Validation Feature ==========
const errors = ref({})

const isValid = computed(() => Object.keys(errors.value).length === 0)
const canSubmit = computed(() => isValid.value && hasChanges.value && !userLoading.value)

function validateField(field) {
  // validation logic
}

watch(formData, () => validateAll(), { deep: true })

// ========== Submit ==========
async function submitForm() {
  if (!canSubmit.value) return
  // submit logic
}

onMounted(fetchUser)
</script>

All user-related code is together. All form code is together. All validation code is together. You can even extract each section into a composable.

Composables: The Killer Feature

Composables are reusable functions that encapsulate reactive state and logic. They're the Composition API's answer to mixins—but better in every way.

Example: A Reusable Form Composable

// composables/useForm.ts
import { ref, computed, watch } from 'vue'

export function useForm<T extends Record<string, any>>(initialData: T) {
  const data = ref({ ...initialData }) as Ref<T>
  const originalData = ref({ ...initialData }) as Ref<T>
  const errors = ref<Record<string, string>>({})
  const isDirty = ref(false)

  const isValid = computed(() => Object.keys(errors.value).length === 0)
  
  const hasChanges = computed(() => 
    JSON.stringify(data.value) !== JSON.stringify(originalData.value)
  )

  function setField<K extends keyof T>(field: K, value: T[K]) {
    data.value[field] = value
    isDirty.value = true
  }

  function setError(field: string, message: string) {
    errors.value[field] = message
  }

  function clearError(field: string) {
    delete errors.value[field]
  }

  function reset() {
    data.value = { ...originalData.value }
    errors.value = {}
    isDirty.value = false
  }

  function setOriginal(newData: T) {
    originalData.value = { ...newData }
    data.value = { ...newData }
    isDirty.value = false
  }

  return {
    data,
    errors,
    isDirty,
    isValid,
    hasChanges,
    setField,
    setError,
    clearError,
    reset,
    setOriginal
  }
}

Using the Composable

<script setup>
import { useForm } from '@/composables/useForm'
import { useUser } from '@/composables/useUser'

// Clean, declarative setup
const { user, loading, fetchUser } = useUser()
const { data, errors, isValid, hasChanges, setField, reset } = useForm({
  name: '',
  email: ''
})

// Watch user changes and update form
watch(user, (newUser) => {
  if (newUser) {
    setOriginal({ name: newUser.name, email: newUser.email })
  }
})
</script>

Mixins (Options API)

  • × Unclear where properties come from
  • × Name collisions between mixins
  • × Hard to pass parameters
  • × TypeScript struggles with typing

Composables (Composition API)

  • Explicit imports—you see what you're using
  • Rename on import to avoid collisions
  • Pass any parameters you need
  • Full TypeScript inference

<script setup>: The Best of Both Worlds

<script setup> is a compile-time syntactic sugar that makes Composition API even more concise. It's the recommended syntax for new Vue 3 projects.

Comparison: setup() vs <script setup>

setup() function

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

export default {
  props: {
    initialCount: Number
  },
  emits: ['update'],
  setup(props, { emit }) {
    const count = ref(props.initialCount)
    const doubled = computed(() => count.value * 2)
    
    function increment() {
      count.value++
      emit('update', count.value)
    }
    
    // Must return everything used in template
    return {
      count,
      doubled,
      increment
    }
  }
}
</script>

<script setup>

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

const props = defineProps<{
  initialCount: number
}>()

const emit = defineEmits<{
  update: [value: number]
}>()

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

function increment() {
  count.value++
  emit('update', count.value)
}

// No return needed - everything is auto-exposed
</script>

Benefits of <script setup>:

  • • Less boilerplate—no export default, no return
  • • Better TypeScript integration with defineProps/defineEmits
  • • Better runtime performance (compiled to more efficient code)
  • • Variables are automatically available in template

TypeScript: A Clear Winner

If you're using TypeScript—or planning to—Composition API is the clear choice. The difference in developer experience is significant.

Options API + TypeScript

<script lang="ts">
import { defineComponent, PropType } from 'vue'

interface User {
  id: number
  name: string
}

export default defineComponent({
  props: {
    user: {
      type: Object as PropType<User>,
      required: true
    }
  },
  data() {
    return {
      count: 0 // Type inferred
    }
  },
  computed: {
    // Need explicit return type sometimes
    greeting(): string {
      return `Hello, ${this.user.name}`
    }
  },
  methods: {
    // 'this' can be tricky to type
    increment() {
      this.count++ // Sometimes TS struggles here
    }
  }
})
</script>

Composition API + TypeScript

<script setup lang="ts">
interface User {
  id: number
  name: string
}

const props = defineProps<{
  user: User
}>()

// Type inference just works
const count = ref(0)

// Computed types are inferred
const greeting = computed(() => 
  `Hello, ${props.user.name}`
)

// No 'this' context issues
function increment() {
  count.value++
}
</script>

Decision Framework: Which API for Your Project?

Answer These Questions:

1. Are you starting a new project or migrating from Vue 2?

New project

→ Use Composition API with <script setup>

Vue 2 migration

→ Start with Options API, migrate gradually

2. What's your team's Vue experience level?

New to Vue

→ Options API for initial learning, then Composition

Experienced

→ Composition API for better code organization

3. Are you using TypeScript?

Yes

→ Strongly prefer Composition API

No

→ Either works well

4. How complex are your components?

Simple, self-contained

→ Options API is fine

Complex, shared logic

→ Composition API with composables

Can You Mix Both APIs?

Yes! You can use both APIs in the same project, and even in the same component (though that's rarely necessary). This is useful during gradual migration.

Using Composables in Options API

<script>
import { useMousePosition } from '@/composables/useMousePosition'

export default {
  // Options API component using a Composition API composable
  setup() {
    const { x, y } = useMousePosition()
    return { x, y }
  },
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>

This is a great migration strategy: start adopting composables in existing Options API components before fully converting them.

Need Help with Your Vue 3 Migration?

Whether you're adopting Composition API, migrating from Vue 2, or converting your entire codebase—we've done it dozens of times. Get a comprehensive audit and fixed-price quote for your project.

✓ Fixed-price guarantee ✓ Includes Composition API conversion ✓ 7-day turnaround

Conclusion

The Composition API vs Options API debate doesn't have to be all-or-nothing. For new Vue 3 projects, we recommend defaulting to Composition API with <script setup>—it's more flexible, has better TypeScript support, and scales better with complex components.

But Options API isn't going away. It's still fully supported, easier for beginners, and works great for simple components. If you're migrating from Vue 2, using Options API initially makes the transition smoother.

The key is understanding the tradeoffs and choosing based on your project's needs—not dogma. And remember: you can always adopt Composition API incrementally, one component at a time.

Related Guides