Frontend / Vue / 05_composition_vs_options.md

Composition API vs Options API (and Migration)

Updated 4 interview angles 6 min read source
On this page12
  • Show the same component in both styles.
  • Why was the Composition API added?
  • What’s ?
  • Can you use both APIs in the same project? Same component?
  • How do composables compare to mixins?
  • When does Options API still make sense?
  • Migration path Vue 2 → Vue 3 with Composition API?
  • TypeScript ergonomics — what’s different?
  • Gotchas
  • Related
  • Further reading
  • Interview angle
  • <

    Composition API vs Options API (and Migration)

    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.

    Show the same component in both styles.

    Options API

    vue
    <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>)

    vue
    <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.

    Why was the Composition API added?

    Three concrete problems with the Options API at scale:

    1. 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.
    2. 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.
    3. TypeScript inference is limited by the Options API’s structure — this typing is complex, options are weakly typed.

    Composition API solves these:

    1. Composables are just functions returning refs/methods — compose, no collisions, explicit sources.
    2. Related logic stays together in setup.
    3. Native TS — no special inference; functions and refs type naturally.

    What’s <script setup>?

    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).

    vue
    <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:

    vue
    <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.

    Can you use both APIs in the same project? Same component?

    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.

    How do composables compare to mixins?

    ts
    // 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:

    ts
    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.

    When does Options API still make sense?

    • 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.

    Migration path Vue 2 → Vue 3 with Composition API?

    1. Upgrade to Vue 3 with Options API kept. Vue 3 supports the Options API fully — most components work as-is with minor changes (see Lifecycle Hooks (Composition + Options Side by Side) for lifecycle rename: beforeDestroy → beforeUnmount).
    2. Fix the Vue 3 breaking changes:
      • Global API moved (new Vue() → createApp()).
      • v-model defaults changed (now uses modelValue/update:modelValue).
      • Filters removed.
      • Functional components are now plain functions.
      • $listeners merged into $attrs.
    3. Refactor to Composition API incrementally — leaf components first, then containers. Composables for shared logic emerge naturally.
    4. 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.

    TypeScript ergonomics — what’s different?

    ts
    // 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

    • this doesn’t exist in setup() — there’s no component instance. Use the function parameter (setup(props, ctx)) for emit/attrs/slots/expose.
    • Top-level await in <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 / defineEmits are compiler macros, not imports. Don’t try to assign them to variables or use them dynamically.
    • reactive props don’t exist — props are always read-only. To make a prop reactive locally, copy via ref(props.x) and watch to sync.

    Further reading

    Interview angle 4

    • “Composition API or Options API?” - Composition with <script setup> is the modern default: related logic stays in one place instead of being scattered across data, computed, methods and watch, reuse is a plain function rather than a mixin, and TypeScript inference works without helpers.
      • Follow-up: “Is Options API deprecated?” - no, it is fully supported in Vue 3 and migration is file by file. There is no rewrite mandate, and a stable Options component is not a defect.
    • “What does <script setup> actually do?” - it is compile-time sugar. The whole block is the setup function, top-level bindings are exposed to the template automatically, and defineProps/defineEmits are compiler macros rather than imported functions - which is why they cannot be used conditionally.
    • “Why are composables better than mixins?” - they are plain functions with explicit inputs and outputs. You can see where every value came from, two of them cannot silently collide on a name, and they are testable in isolation. Mixins merged into the component and the collisions were invisible.
    • “Can you mutate a prop?” - no, props are read-only. If you need a local editable copy, initialise a ref from it and watch the prop to resync - which also forces you to decide explicitly what happens when the parent changes it mid-edit.