Skip to main content

Options API and composition API

Options API organizes Vue components via named object properties like data and methods. Composition API groups logic by feature using imported functions inside setup().

Theory

TL;DR

  • Options API = filing cabinet with labeled drawers (data here, methods there, computed elsewhere)
  • Composition API = whiteboard with sticky notes grouped by task (counter logic stays in one block)
  • Main difference: Options scatters related code across the object; Composition keeps it together
  • Small component or prototype? Options works fine. Logic reuse or 500+ lines? Composition
  • TypeScript project? Composition gives better type inference with defineProps<{...}>()

Quick example

Options API splits counter logic across sections:

vue
<script> export default { data() { return { count: 0 }; }, computed: { double() { return this.count * 2; } }, methods: { increment() { this.count++; } } }; </script> <!-- data, computed, and methods are in separate sections --> <!-- but they all belong to the same counter feature -->

Composition API groups the same logic together:

vue
<script setup> import { ref, computed } from 'vue'; const count = ref(0); const double = computed(() => count.value * 2); const increment = () => count.value++; </script> <!-- all counter logic in one place -->

Both produce identical output. The difference is where you find related code when you open the file.

Key difference

Options API structures code by type: reactive data in data(), functions in methods, derived values in computed. Counter-related state, methods, and computed values sit in three different sections. Composition API structures by purpose: everything the counter needs lives in one block. In practice, the switch usually happens after the third time you scroll through a 400-line file hunting for the method that touches a specific piece of state.

When to use

  • New to Vue or building a simple widget: Options. Predictable structure, no new mental model required.
  • Reusing logic across multiple components: Composition. Extract to a composable like useCounter() or useFetch().
  • Component growing past 300-500 lines: Composition. Jumping between data, methods, and computed for one feature gets exhausting.
  • TypeScript-heavy codebase: Composition. <script setup> infers prop types with defineProps<{ name: string }>() automatically. Options needs full ComponentOptions declaration merging.
  • Vue 2 project or migration path: Options. Direct compatibility, no extra packages needed.
  • Pinia stores or Nuxt 3 composables: Composition. Both are built around this model.

Comparison table

AspectOptions APIComposition API
StructureBy option type (data/methods/computed)By feature (counter logic grouped)
Reactivitythis.count (auto-tracked)ref(0) or reactive({}), needs .value
Logic reuseCopy methods across components manuallyComposables (useCounter(), useFetch())
TypeScriptVerbose ComponentOptions declarationsNative inference in <script setup>
Best file sizeCompact for small componentsScales well for large, complex files
When to usePrototypes, Vue 2 apps, simple widgetsProduction apps, shared logic, TypeScript

How it works internally

Vue 3 parses Options API into a proxy-wrapped this context and auto-binds reactivity via Object.defineProperty on data properties during component init. Composition API runs setup() first, collecting ref and reactive proxies into a shared render context. The dependency scheduler works identically during template compilation in both cases. The structural difference is that Composition logic lives in JavaScript scope chains instead of hanging off this.

Common mistakes

Mutating a ref without .value:

js
const count = ref(0); count++; // stays 0, you incremented the wrapper object, not the value

ref returns { value: 0 }. Direct mutation bypasses reactivity entirely. Fix: count.value++.

Accessing this inside setup():

js
export default { setup() { console.log(this.count); // undefined } }

setup() runs before the this proxy exists. Nothing to bind to. Fix: return refs from setup, or use <script setup>.

Assuming reactive tracks deep nested additions:

js
const state = reactive({ items: [] }); state.items.push({ count: 0 }); // { count: 0 } is NOT reactive

reactive wraps the top-level object only. Objects pushed in later need their own ref or reactive. Fix: state.items.push({ count: ref(0) }).

Mixing both APIs in one component:

js
export default { data() { return { x: 0 }; }, setup() { return { x: ref(0) }; } // Options x overrides setup x }

Shadowing. Options properties override setup return values with the same name. Pick one API per component.

Real-world usage

  • Nuxt 3: Composition throughout. useFetch, useRoute, useAsyncData are all composables.
  • Pinia: defineStore uses Composition-style with ref and computed inside.
  • VitePress: <script setup> for all interactive documentation demos.
  • Element Plus: Migrating complex logic to Composition for tree-shaking support.
  • Vuetify: Options for basic components, Composition for custom plugin logic.

Follow-up questions

Q: What is a composable?


A: A function that uses Composition API internally and returns reactive state or methods. Example: function useCounter() { const count = ref(0); return { count, increment: () => count.value++ }; }. The Composition equivalent of custom React hooks.

Q: What is the difference between ref and reactive?


A: ref works for primitives and objects, auto-unwraps in templates but needs .value in script. reactive works for objects only, allows direct property access, but loses reactivity on destructuring. Use ref for count, reactive for { user, settings }.

Q: Can you still use Options API in Vue 3?


A: Yes. Options API is fully supported in Vue 3 and the Vue team has no plans to remove it. Composition API is an addition, not a replacement.

Q: Why is Composition API better for TypeScript?


A: <script setup> with defineProps<{ name: string }>() infers types directly from the generic parameter. Options API requires verbose ComponentOptions declaration merging to achieve the same. That is why TypeScript-heavy teams default to Composition.

Q: Walk me through converting an Options component to Composition. (Senior)


A: Map data properties to ref or reactive, computed to computed() calls, methods to plain functions, lifecycle hooks like mounted to onMounted. Use <script setup> to skip the return statement entirely.

Examples

Basic counter: Options vs Composition

Options API splits the counter across three sections:

vue
<template> <p>Counter: {{ count }} | Double: {{ double }}</p> <button @click="increment">+</button> </template> <script> export default { data() { return { count: 0 }; }, computed: { double() { return this.count * 2; } }, methods: { increment() { this.count++; } } }; </script>

Composition API keeps the same logic together:

vue
<template> <p>Counter: {{ count }} | Double: {{ double }}</p> <button @click="increment">+</button> </template> <script setup> import { ref, computed } from 'vue'; const count = ref(0); const double = computed(() => count.value * 2); const increment = () => count.value++; </script>

Both render identically. In the Options version, understanding the counter means reading three separate sections. In Composition, four consecutive lines.

Todo list with filtering

This is where the difference becomes real. Options scatters filter logic across the file:

vue
<script> export default { data() { return { todos: [], filter: 'all' // state lives here }; }, computed: { filteredTodos() { // derived logic here, far from state return this.todos.filter(t => this.filter === 'all' || t.done === (this.filter === 'done') ); } }, methods: { addTodo(text) { // mutations even further away this.todos.push({ text, done: false }); } } }; </script>

Composition keeps everything together and makes extraction easy:

vue
<script setup> import { ref, computed } from 'vue'; // All todo logic in one block, easy to move to useTodos.js const todos = ref([]); const filter = ref('all'); const filteredTodos = computed(() => todos.value.filter(t => filter.value === 'all' || t.done === (filter.value === 'done') ) ); const addTodo = (text) => todos.value.push({ text, done: false }); </script>

To share this logic with another component, you cut the block and drop it in a useTodos.js file. With Options, you copy from three separate places.

Composable pattern

The production payoff. Extract to useTodos.js:

js
// useTodos.js import { ref, computed } from 'vue'; export function useTodos() { const todos = ref([]); const filter = ref('all'); const filteredTodos = computed(() => todos.value.filter(t => filter.value === 'all' || t.done === (filter.value === 'done') ) ); const addTodo = (text) => todos.value.push({ text, done: false }); return { todos, filter, filteredTodos, addTodo }; }

Any component imports it in two lines:

vue
<script setup> import { useTodos } from './useTodos'; const { todos, filter, filteredTodos, addTodo } = useTodos(); </script>

Options API has no real equivalent. Mixins existed, but name collisions and opaque data sources made them painful. Composables replaced them completely.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?