| |

Vue.js 4 🟢 Options API Overview vs Composition API Paradigm Shift

Vue 3 ships with two ways to write components: the Options API and the Composition API. The Options API organizes a component by option type: data, methods, computed, watch, and lifecycle hooks each get their own section. The Composition API organizes a component by logical concern: everything related to a feature lives together in a setup function, regardless of whether it is state, a computed value, or a side effect. Both are fully supported, both are production-ready, and both can be mixed in the same component. The question is not which one is correct but which one fits the component and the team.

The Options API has been the default since Vue 2, and it remains the more approachable style for simple components. The Composition API was introduced in Vue 3 to solve a specific problem: as components grow, the Options API scatters related logic across separate options, and the code that belongs together ends up apart. The Composition API groups by concern instead. It also enables logic extraction into composable functions, which the Options API cannot do cleanly. This chapter compares the two, explains what each is good at, and describes the mental shift required to move from one to the other.

Key point: The Options API organizes by option type (data, methods, computed, watch). The Composition API organizes by logical concern inside setup or <script setup>. Both are supported in Vue 3 and can coexist. The Composition API is better for large components and shared logic. The Options API is better for simple components and teams new to Vue.


Why two APIs exist

The backward compatibility problem. Vue 3 needed to keep the Options API because millions of Vue 2 components use it. Removing it would have broken the ecosystem. Keeping it meant the framework had to support both styles, and the team committed to that from the start.

The scaling problem. The Options API works well for small components but degrades as components grow. A component with a search feature, a pagination feature, and a filtering feature has its search state in data, its search logic in methods, its search watcher in watch, and its search lifecycle in mounted. The code that belongs together is spread across four options. The Composition API allows all the search-related code to live in one block.

The reuse problem. The Options API reuses logic through mixins, which merge their options into the component. Mixins have well-known problems: unclear property origins, naming collisions, and implicit dependencies. The Composition API reuses logic through composable functions, which are explicit and return only what the caller needs.

The type inference problem. In the Options API, TypeScript cannot always infer the types of this inside methods and computed properties. The Composition API is plain functions and variables, so TypeScript infers types naturally. This is one reason the Composition API became the default for new Vue 3 projects.

The mental model problem. The Options API is declarative: you fill in the options the framework expects. The Composition API is imperative: you write the code that creates the state, the computed values, and the effects. The shift is from “which option does this go in?” to “what does this feature need?”


a. The Options API structure

An Options API component is an object with named options. Each option has a specific role, and the framework calls them at the appropriate time.

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

export default defineComponent({
  name: 'UserCard',
  props: {
    userId: { type: String, required: true },
  },
  data() {
    return {
      user: null as User | null,
      loading: false,
      error: null as string | null,
    };
  },
  computed: {
    displayName(): string {
      return this.user ? `${this.user.firstName} ${this.user.lastName}` : '';
    },
  },
  watch: {
    userId: {
      immediate: true,
      handler(newId: string) {
        this.loadUser(newId);
      },
    },
  },
  methods: {
    async loadUser(id: string) {
      this.loading = true;
      this.error = null;
      try {
        this.user = await fetchUser(id);
      } catch (err) {
        this.error = String(err);
      } finally {
        this.loading = false;
      }
    },
  },
  mounted() {
    console.log('UserCard mounted');
  },
});
</script>

<template>
  <div v-if="loading">Loading...</div>
  <div v-else-if="error">{{ error }}</div>
  <div v-else>{{ displayName }}</div>
</template>

The component has a userId prop, three pieces of state in data, a computed displayName, a watcher on userId, a method loadUser, and a mounted hook. The logic for loading the user is split across data, watch, methods, and mounted. Everything about “loading the user” is in three different options.

The Options API is readable when the component is small. The reader can find data, methods, and computed by scanning the sections. The framework handles the wiring.


b. The Composition API structure

A Composition API component uses setup or, more commonly, <script setup>. The state, computed values, watchers, and lifecycle hooks are all defined in the same scope, organized by concern.

<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue';

const props = defineProps<{ userId: string }>();

const user = ref<User | null>(null);
const loading = ref(false);
const error = ref<string | null>(null);

const displayName = computed(() =>
  user.value ? `${user.value.firstName} ${user.value.lastName}` : ''
);

async function loadUser(id: string) {
  loading.value = true;
  error.value = null;
  try {
    user.value = await fetchUser(id);
  } catch (err) {
    error.value = String(err);
  } finally {
    loading.value = false;
  }
}

watch(
  () => props.userId,
  (newId) => loadUser(newId),
  { immediate: true }
);

onMounted(() => {
  console.log('UserCard mounted');
});
</script>

<template>
  <div v-if="loading">Loading...</div>
  <div v-else-if="error">{{ error }}</div>
  <div v-else>{{ displayName }}</div>
</template>

The same component, written with the Composition API, has all the user-loading logic in one place. The user, loading, and error refs are declared together. The loadUser function is next to them. The watcher that triggers it is next. The reader does not have to jump between sections.

The <script setup> syntax is compiled into a setup function, and the top-level bindings are automatically exposed to the template. There is no need to return anything.


c. The mental shift

Moving from the Options API to the Composition API is not a syntax change; it is a change in how the component is organized.

Question the Options API answersQuestion the Composition API answers
What data does the component have?What concern am I implementing?
What methods does it expose?What state does this concern need?
What computed values does it derive?What computed values does it derive?
What does it watch?What does it need to react to?
What lifecycle hooks does it use?When does this concern start and stop?

The Options API organizes by “what kind of thing is this?” The Composition API organizes by “what does this feature need?” The first is a taxonomy; the second is a narrative.

For a small component, the taxonomy is fine. For a component with three features, the narrative keeps the code readable. This is why the Composition API is recommended for components that grow beyond a certain size, and why it is required for logic that is extracted and reused.


d. Reuse: mixins versus composables

The Options API reuses logic through mixins. A mixin is an object with the same options as a component, and when a component uses a mixin, the mixin’s options are merged into the component’s.

// mixins/useMouse.js
export const useMouse = {
  data() {
    return { x: 0, y: 0 };
  },
  mounted() {
    window.addEventListener('mousemove', this.update);
  },
  beforeUnmount() {
    window.removeEventListener('mousemove', this.update);
  },
  methods: {
    update(e: MouseEvent) {
      this.x = e.clientX;
      this.y = e.clientY;
    },
  },
};

The component uses the mixin:

<script>
import { useMouse } from './mixins/useMouse';

export default {
  mixins: [useMouse],
};
</script>

The problems with mixins are well documented. The component’s data now contains x and y, but it is not obvious where they came from. If two mixins both define x, the merge order determines which one wins. The mixin’s dependencies on this are implicit. The types are difficult to infer.

The Composition API reuses logic through composables. A composable is a function that returns reactive state and functions.

// composables/useMouse.ts
import { ref, onMounted, onBeforeUnmount } from 'vue';

export function useMouse() {
  const x = ref(0);
  const y = ref(0);

  function update(e: MouseEvent) {
    x.value = e.clientX;
    y.value = e.clientY;
  }

  onMounted(() => window.addEventListener('mousemove', update));
  onBeforeUnmount(() => window.removeEventListener('mousemove', update));

  return { x, y };
}

The component uses the composable:

<script setup lang="ts">
import { useMouse } from './composables/useMouse';

const { x, y } = useMouse();
</script>

The state is explicit. The component names x and y at the call site. If two composables both return x, the developer aliases one of them with destructuring. The dependencies are in the function body. TypeScript infers the types from the return value.

Composables are the primary reason the Composition API exists. They make logic reusable in a way that mixins cannot, and the reuse is explicit rather than merged.


e. When to use which

The two APIs are not mutually exclusive. A component can use the Options API, the Composition API, or both. The choice depends on the component and the team.

SituationRecommendation
Simple component, few state variablesOptions API
Component with multiple featuresComposition API
Logic that will be reusedComposition API (composable)
Team new to VueOptions API to start
Team with TypeScript experienceComposition API
Library or design system componentComposition API for the API surface
Existing Vue 2 componentKeep the Options API; migrate later

The Vue team’s recommendation is that the Composition API is the default for new projects, but the Options API remains supported and is not deprecated. A codebase can mix the two. A component can use the Options API and, in the same component, call a composable from setup.

<script lang="ts">
import { defineComponent } from 'vue';
import { useMouse } from './composables/useMouse';

export default defineComponent({
  setup() {
    const { x, y } = useMouse();
    return { x, y };
  },
  data() {
    return { count: 0 };
  },
});
</script>

This hybrid is legal and occasionally useful during a migration, but mixing the two styles within a single component reduces readability. It is a transitional pattern, not a destination.


Complete Example Session

<!-- ============================================
PART 1: OPTIONS API COMPONENT
============================================ -->
<script lang="ts">
import { defineComponent } from 'vue';

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

<template>
  <p>{{ count }} × 2 = {{ doubled }}</p>
  <button @click="increment">+1</button>
</template>
<!-- ============================================
PART 2: COMPOSITION API COMPONENT
============================================ -->
<script setup lang="ts">
import { ref, computed } from 'vue';

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

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

<template>
  <p>{{ count }} × 2 = {{ doubled }}</p>
  <button @click="increment">+1</button>
</template>
<!-- ============================================
PART 3: OPTIONS API WITH WATCHER
============================================ -->
<script lang="ts">
export default {
  data() {
    return { query: '', results: [] };
  },
  watch: {
    query(newQuery: string) {
      this.search(newQuery);
    },
  },
  methods: {
    async search(q: string) {
      this.results = await fetchResults(q);
    },
  },
};
</script>
<!-- ============================================
PART 4: COMPOSITION API WITH WATCHER
============================================ -->
<script setup lang="ts">
import { ref, watch } from 'vue';

const query = ref('');
const results = ref([]);

watch(query, async (newQuery) => {
  results.value = await fetchResults(newQuery);
});
</script>
// ============================================
// PART 5: MIXIN (OPTIONS API REUSE)
// ============================================
export const useMouse = {
  data() {
    return { x: 0, y: 0 };
  },
  mounted() {
    window.addEventListener('mousemove', this.update);
  },
  beforeUnmount() {
    window.removeEventListener('mousemove', this.update);
  },
  methods: {
    update(e: MouseEvent) {
      this.x = e.clientX;
      this.y = e.clientY;
    },
  },
};
// ============================================
// PART 6: COMPOSABLE (COMPOSITION API REUSE)
// ============================================
import { ref, onMounted, onBeforeUnmount } from 'vue';

export function useMouse() {
  const x = ref(0);
  const y = ref(0);

  function update(e: MouseEvent) {
    x.value = e.clientX;
    y.value = e.clientY;
  }

  onMounted(() => window.addEventListener('mousemove', update));
  onBeforeUnmount(() => window.removeEventListener('mousemove', update));

  return { x, y };
}
<!-- ============================================
PART 7: USING A COMPOSABLE
============================================ -->
<script setup lang="ts">
import { useMouse } from './composables/useMouse';

const { x, y } = useMouse();
</script>

<template>
  <p>Mouse: {{ x }}, {{ y }}</p>
</template>
<!-- ============================================
PART 8: HYBRID COMPONENT
============================================ -->
<script lang="ts">
import { defineComponent } from 'vue';
import { useMouse } from './composables/useMouse';

export default defineComponent({
  setup() {
    const { x, y } = useMouse();
    return { x, y };
  },
  data() {
    return { count: 0 };
  },
});
</script>
<!-- ============================================
PART 9: COMPOSITION API WITH LIFECYCLE
============================================ -->
<script setup lang="ts">
import { onMounted, onBeforeUnmount } from 'vue';

onMounted(() => {
  console.log('mounted');
});

onBeforeUnmount(() => {
  console.log('unmounting');
});
</script>
// ============================================
// PART 10: EXTRACTED COMPOSABLE
// ============================================
export function useFetch<T>(url: string) {
  const data = ref<T | null>(null);
  const loading = ref(false);
  const error = ref<string | null>(null);

  async function load() {
    loading.value = true;
    error.value = null;
    try {
      data.value = await fetch(url).then((r) => r.json());
    } catch (err) {
      error.value = String(err);
    } finally {
      loading.value = false;
    }
  }

  onMounted(load);

  return { data, loading, error, reload: load };
}

These ten parts cover an Options API counter, a Composition API counter, an Options API watcher, a Composition API watcher, a mixin, a composable, using a composable, a hybrid component, Composition API lifecycle hooks, and an extracted composable.


Quick Reference

Structure Comparison

AspectOptions APIComposition API
OrganizationBy option typeBy logical concern
Statedata()ref, reactive
Computedcomputed optioncomputed() function
Watchwatch optionwatch() function
Methodsmethods optionPlain functions
Lifecyclemounted, unmountedonMounted, onUnmounted
Propsprops optiondefineProps
Emitsemits optiondefineEmits
ReuseMixinsComposables
TypeScriptLimited inferenceFull inference

Options API Options

OptionPurpose
nameComponent name
propsDeclare props
emitsDeclare emits
dataReactive state
computedDerived state
watchReactive side effects
methodsFunctions
mounted / unmountedLifecycle hooks

Composition API Functions

FunctionPurpose
refReactive primitive
reactiveReactive object
computedDerived state
watchReactive side effects
onMountedLifecycle hook
onUnmountedLifecycle hook
definePropsDeclare props
defineEmitsDeclare emits

Reuse Comparison

AspectMixinComposable
MergeImplicitExplicit
OriginUnclearNamed at call site
CollisionSilent overrideAliased by caller
TypesDifficultInferred
DependenciesImplicit on thisIn function body

Choosing an API

SituationRecommendation
Simple componentOptions API
Large componentComposition API
Reusable logicComposition API
New to VueOptions API to start
TypeScript-heavyComposition API
Existing Vue 2 codeOptions API, migrate later

Best Practices

✅ Do This:

<!-- Use <script setup> for new components -->
<script setup lang="ts">
import { ref, computed } from 'vue';
const count = ref(0);
const doubled = computed(() => count.value * 2);
</script>

<!-- Extract logic into composables -->
export function useMouse() {
  const x = ref(0);
  const y = ref(0);
  return { x, y };
}

<!-- Group related code together -->
const user = ref(null);
const loading = ref(false);
async function loadUser() { }
watch(() => props.userId, loadUser);

❌ Don’t Do This:

<!-- Mix APIs in the same component without reason -->
<script>
export default {
  setup() { /* ... */ },
  data() { /* ... */ },  // ❌ confusing
};
</script>

<!-- Use mixins for new code -->
mixins: [useMouse]  // ❌ use composables instead

<!-- Put unrelated logic in one composable -->
export function useEverything() { }  // ❌ split by concern

<!-- Access this in <script setup> -->
console.log(this);  // ❌ no this in setup

Common Pitfalls

PitfallWhy It HappensFix
this is undefinedUsed in <script setup>Use the Composition API directly
Reactivity lostDestructured a reactive objectUse toRefs or access properties
Props not updatingDestructured propsUse props.x or toRef
Composable called conditionallyViolates the rules of hooksCall composables at the top level
Mixin collisionTwo mixins define the same propertyUse composables with explicit names
Lifecycle hook not firingRegistered outside setupCall inside setup or <script setup>

Real-World Examples

1. Options API Counter

<script>
export default {
  data: () => ({ count: 0 }),
  methods: { increment() { this.count++; } },
};
</script>

2. Composition API Counter

<script setup>
import { ref } from 'vue';
const count = ref(0);
const increment = () => count.value++;
</script>

3. Options API Computed

<script>
export default {
  data: () => ({ first: '', last: '' }),
  computed: {
    fullName() { return `${this.first} ${this.last}`; },
  },
};
</script>

4. Composition API Computed

<script setup>
import { ref, computed } from 'vue';
const first = ref('');
const last = ref('');
const fullName = computed(() => `${first.value} ${last.value}`);
</script>

5. Composables

export function useCounter(initial = 0) {
  const count = ref(initial);
  const increment = () => count.value++;
  return { count, increment };
}

6. Using a Composable

<script setup>
import { useCounter } from './composables/useCounter';
const { count, increment } = useCounter(10);
</script>

7. Options API Watcher

<script>
export default {
  watch: {
    query(newQ) { this.search(newQ); },
  },
};
</script>

8. Composition API Watcher

<script setup>
import { watch, ref } from 'vue';
const query = ref('');
watch(query, (newQ) => search(newQ));
</script>

9. Lifecycle in Options API

<script>
export default {
  mounted() { console.log('mounted'); },
  beforeUnmount() { console.log('leaving'); },
};
</script>

10. Lifecycle in Composition API

<script setup>
import { onMounted, onBeforeUnmount } from 'vue';
onMounted(() => console.log('mounted'));
onBeforeUnmount(() => console.log('leaving'));
</script>

Visual

Organization by Concern

┌──────────────────────────────────────────────────────────────┐
│  OPTIONS API:                                                │
│  data:    { user, loading, error, query, results }           │
│  methods: { loadUser, search }                               │
│  computed:{ displayName, filteredResults }                   │
│  watch:   { userId, query }                                  │
│                                                              │
│  User logic is in data + methods + watch + mounted.          │
│  Search logic is in data + methods + watch.                  │
│  The two concerns are interleaved.                           │
│                                                              │
│  COMPOSITION API:                                            │
│  // User concern                                             │
│  const user = ref(null);                                     │
│  const loading = ref(false);                                 │
│  async function loadUser() { }                               │
│  watch(() => props.userId, loadUser);                        │
│                                                              │
│  // Search concern                                           │
│  const query = ref('');                                      │
│  const results = ref([]);                                    │
│  watch(query, search);                                       │
│                                                              │
│  Each concern is a contiguous block.                         │
└──────────────────────────────────────────────────────────────┘

Reuse: Mixin vs Composable

┌──────────────────────────────────────────────────────────────┐
│  MIXIN:                                                      │
│  mixins: [useMouse, useKeyboard]                             │
│  └── Properties merge into the component                     │
│  └── Origin is unclear                                       │
│  └── Collisions are silent                                   │
│                                                              │
│  COMPOSABLE:                                                 │
│  const { x, y } = useMouse();                                │
│  const { key } = useKeyboard();                              │
│  └── Return values are explicit                              │
│  └── Call site names them                                    │
│  └── Collisions are resolved by aliasing                     │
└──────────────────────────────────────────────────────────────┘

Mental Model

┌──────────────────────────────────────────────────────────────┐
│  OPTIONS API asks:                                           │
│  "What kind of thing is this code?"                          │
│  - data, methods, computed, watch, lifecycle                 │
│                                                              │
│  COMPOSITION API asks:                                       │
│  "What feature does this code implement?"                    │
│  - user loading, search, pagination, authentication          │
│                                                              │
│  Taxonomy vs narrative.                                      │
└──────────────────────────────────────────────────────────────┘

Hybrid Component

┌──────────────────────────────────────────────────────────────┐
│  <script lang="ts">                                          │
│  export default defineComponent({                            │
│    setup() {                                                 │
│      const { x, y } = useMouse();  // composable             │
│      return { x, y };                                        │
│    },                                                        │
│    data() {                                                  │
│      return { count: 0 };          // options                │
│    },                                                        │
│  });                                                         │
│  </script>                                                   │
│                                                              │
│  Legal, but a transitional pattern.                          │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Options APIOrganizes by option type
Composition APIOrganizes by logical concern
dataState in Options API
ref / reactiveState in Composition API
computedDerived state in both
watchSide effects in both
Lifecyclemounted vs onMounted
Reuse (Options)Mixins
Reuse (Composition)Composables
Default for new projectsComposition API
Both supportedYes

Key takeaways:

  • The Options API organizes by option type. data, methods, computed, watch, and lifecycle hooks each get their own section. It is readable for simple components and familiar to Vue 2 developers.
  • The Composition API organizes by logical concern. State, computed values, watchers, and lifecycle hooks for a feature live together in setup or <script setup>. It is better for large components and shared logic.
  • The mental shift is from taxonomy to narrative. The Options API asks “what kind of thing is this code?” The Composition API asks “what feature does this code implement?”
  • Composables replace mixins. A composable is a function that returns reactive state and functions. The reuse is explicit, the types are inferred, and collisions are resolved at the call site.
  • Both APIs are fully supported. A component can use either, and a project can mix them. The Composition API is the recommended default for new projects, but the Options API is not deprecated.
  • The Composition API is required for <script setup>. The syntactic sugar compiles to a setup function, and the top-level bindings are exposed to the template.
  • The choice depends on the component and the team. Simple components can use the Options API. Complex components, shared logic, and TypeScript-heavy projects benefit from the Composition API.

Remember: The Options API and the Composition API are two ways of organizing the same component. The Options API is declarative and organized by option type; the Composition API is imperative and organized by concern. The Composition API was introduced to solve the scaling problem of the Options API and to enable composables, which are the modern replacement for mixins. Vue 3 supports both, and the choice is not permanent: a component can be migrated from one to the other when the need arises. For new projects, the Composition API is the default. For existing Vue 2 code, the Options API remains valid until the component grows large enough to justify the migration. Understanding both is understanding how Vue components are written.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!