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:
<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:
<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()oruseFetch(). - Component growing past 300-500 lines: Composition. Jumping between
data,methods, andcomputedfor one feature gets exhausting. - TypeScript-heavy codebase: Composition.
<script setup>infers prop types withdefineProps<{ name: string }>()automatically. Options needs fullComponentOptionsdeclaration 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
| Aspect | Options API | Composition API |
|---|---|---|
| Structure | By option type (data/methods/computed) | By feature (counter logic grouped) |
| Reactivity | this.count (auto-tracked) | ref(0) or reactive({}), needs .value |
| Logic reuse | Copy methods across components manually | Composables (useCounter(), useFetch()) |
| TypeScript | Verbose ComponentOptions declarations | Native inference in <script setup> |
| Best file size | Compact for small components | Scales well for large, complex files |
| When to use | Prototypes, Vue 2 apps, simple widgets | Production 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:
const count = ref(0);
count++; // stays 0, you incremented the wrapper object, not the valueref returns { value: 0 }. Direct mutation bypasses reactivity entirely. Fix: count.value++.
Accessing this inside setup():
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:
const state = reactive({ items: [] });
state.items.push({ count: 0 }); // { count: 0 } is NOT reactivereactive 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:
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,useAsyncDataare all composables. - Pinia:
defineStoreuses Composition-style withrefandcomputedinside. - 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:
<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:
<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:
<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:
<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:
// 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:
<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 readyA concise answer to help you respond confidently on this topic during an interview.