Composition API vs Options API (and Migration)
TL;DR
Options API: state in data(), methods in methods, computeds in computed, watchers in watch, lifecycle in mounted/updated/etc. Vue 2’s default; still supported in Vue 3. Composition API: everything in a setup function (or <script setup>) using ref, computed, watch, onMounted. Better for reuse via composables and TypeScript — the modern default. The two coexist; you can use either or both in the same project. Migration is incremental, file-by-file.
Interview Q&A
Q: Show the same component in both styles.
A:
Options API:
<script>
export default {
data() {
return { count: 0 };
},
computed: {
double() { return this.count * 2; },
},
methods: {
increment() { this.count++; },
},
mounted() {
console.log("mounted");
},
};
</script>
<template>
<button @click="increment">{{ count }} (×2 = {{ double }})</button>
</template>
Composition API (<script setup>):
<script setup>
import { ref, computed, onMounted } from "vue";
const count = ref(0);
const double = computed(() => count.value * 2);
const increment = () => { count.value++; };
onMounted(() => console.log("mounted"));
</script>
<template>
<button @click="increment">{{ count }} (×2 = {{ double }})</button>
</template>
Same component; different style. <script setup> is the most concise — no boilerplate, top-level bindings are automatically exposed to the template.
Q: Why was the Composition API added?
A: Three concrete problems with the Options API at scale:
- Logic reuse is awkward. Mixins (Vue 2’s mechanism) have name collisions, opaque sources, and don’t compose. Higher-order components are clunky. Renderless components don’t fit all patterns.
- Related logic is scattered across options. A single feature might span
data,computed,methods,mounted,watch— five separate blocks. In a large component, related concerns are split apart. - TypeScript inference is limited by the Options API’s structure —
thistyping is complex, options are weakly typed.
Composition API solves these:
- Composables are just functions returning refs/methods — compose, no collisions, explicit sources.
- Related logic stays together in setup.
- Native TS — no special inference; functions and refs type naturally.
Q: What’s <script setup>?
A: A compile-time shorthand. The Composition API’s setup() is implicitly the whole script block; top-level bindings are auto-exposed to the template; macros like defineProps / defineEmits / defineExpose / defineOptions are compiler-handled (don’t need imports).
<script setup>
import { ref } from "vue";
const props = defineProps<{ title: string }>();
const emit = defineEmits<{ submit: [value: string] }>();
defineExpose({ focus }); // expose methods to parent ref
const value = ref("");
function focus() { /* ... */ }
</script>
Without <script setup>, you’d write:
<script>
import { ref, defineComponent } from "vue";
export default defineComponent({
props: { title: { type: String, required: true } },
emits: ["submit"],
setup(props, { emit, expose }) {
const value = ref("");
function focus() {}
expose({ focus });
return { value };
},
});
</script>
<script setup> is the modern default. Almost no one writes raw setup() in new code.
Q: Can you use both APIs in the same project? Same component?
A: Both APIs in the same project: yes — file by file. Same component: technically yes (you can have a setup() and options like methods), but it’s confusing — pick one per component.
Q: How do composables compare to mixins?
A:
// composable — explicit, composable, TS-friendly
export function useMouse() {
const x = ref(0), y = ref(0);
const update = (e: MouseEvent) => { x.value = e.x; y.value = e.y; };
onMounted(() => window.addEventListener("mousemove", update));
onUnmounted(() => window.removeEventListener("mousemove", update));
return { x, y };
}
// consumer
const { x, y } = useMouse();
vs Vue 2 mixin:
const MouseMixin = {
data() { return { x: 0, y: 0 }; },
mounted() { window.addEventListener("mousemove", this.update); },
methods: { update(e) { this.x = e.x; this.y = e.y; } },
};
// consumer
export default { mixins: [MouseMixin] };
// this.x and this.y exist — but where from? Conflicts? Unclear.
Composables win on: explicit source, no naming conflicts, composable (one composable calls another), TS-typed, easy testing.
Q: When does Options API still make sense?
A:
- Existing Vue 2 codebases — don’t rewrite for the sake of it.
- Junior-heavy teams that find Options API’s structure easier to navigate.
- Strict consistency — if the rest of the project is Options API, match it.
But: new Vue 3 projects almost universally start with <script setup> + Composition API.
Q: Migration path Vue 2 → Vue 3 with Composition API?
A:
- Upgrade to Vue 3 with Options API kept. Vue 3 supports the Options API fully — most components work as-is with minor changes (see 06_lifecycle_hooks.md for lifecycle rename:
beforeDestroy→beforeUnmount). - Fix the Vue 3 breaking changes:
- Global API moved (
new Vue()→createApp()). v-modeldefaults changed (now usesmodelValue/update:modelValue).- Filters removed.
- Functional components are now plain functions.
$listenersmerged into$attrs.
- Global API moved (
- Refactor to Composition API incrementally — leaf components first, then containers. Composables for shared logic emerge naturally.
- Convert SFCs to
<script setup>as a cosmetic last step.
There’s no big-bang rewrite. Many production Vue 3 apps mix both styles for years.
Q: TypeScript ergonomics — what’s different?
A:
// Options API — props typing via runtime declaration + PropType
export default defineComponent({
props: {
user: { type: Object as PropType<User>, required: true },
onSave: { type: Function as PropType<(u: User) => void> },
},
});
// Composition API — pure TS, no runtime cast
<script setup lang="ts">
const props = defineProps<{
user: User;
onSave?: (u: User) => void;
}>();
</script>
Composition API with <script setup lang="ts"> is dramatically cleaner — pure type declarations, no runtime cast gymnastics.
Gotchas / edge cases
thisdoesn’t exist insetup()— there’s no component instance. Use the function parameter (setup(props, ctx)) foremit/attrs/slots/expose.- Top-level
awaitin<script setup>turns the component into an async component (requires<Suspense>upstream). - Composables must be called synchronously during setup — not inside conditionals, async callbacks, or after
await. Same constraint as React hooks; same reason (call order forms the “identity” for lifecycle binding). - Auto-imports (via
unplugin-auto-import) are common in Vue 3 projects —ref,computed, etc. become globally available. Convenient but obscures origins; team convention call. defineProps/defineEmitsare compiler macros, not imports. Don’t try to assign them to variables or use them dynamically.reactiveprops don’t exist — props are always read-only. To make a prop reactive locally, copy viaref(props.x)andwatchto sync.
What a senior is expected to say
- “Composition API +
<script setup>is the modern default. Logic stays together, composables make reuse first-class, TypeScript inference is native — none of which Mixins solved.” - “Options API is still fully supported in Vue 3. Migration is incremental and file-by-file; there’s no rewrite mandate.”
- “
<script setup>is the compile-time shorthand — top-level bindings auto-exposed,defineProps/defineEmitsare macros, the whole script is the setup function.” - “Composables are plain functions returning refs and methods. They’re testable, typeable, and don’t have the name-collision problem mixins had.”
- “Props are always read-only; need a reactive local copy, you
ref(props.x)and sync withwatch.”
Cross-references
- Lifecycle equivalents (Options vs Composition): 06_lifecycle_hooks.md
- Props/emits/v-model details: 07_props_emits_vmodel.md
- Composables (returning refs): 03_ref_vs_reactive.md
Further reading
- Vue docs — Composition API FAQ: https://vuejs.org/guide/extras/composition-api-faq.html
- Vue docs —
<script setup>: https://vuejs.org/api/sft-script-setup.html - Vue 3 Migration Guide: https://v3-migration.vuejs.org/