Hook — 05 : 38 a.m., Medellín ↔ Playa del Carmen pairing sprint
A humid dawn breeze rolled through my Colombian apartment when Andrés, coding from a beach café in Playa del Carmen, pinged me: “Our Options-API component looks like spaghetti; the new intern can’t follow the reactivity.” I watched his screen over shaky Wi-Fi: data, computed, watchers, and methods scattered like dominoes. We refactored the same logic with Vue 3’s Composition API—grouped by feature, powered by<script setup>
, and fully typed—then hot-reloaded. The bundle size stayed lean, scroll jank vanished, and the intern dropped a celebratory 🌮 emoji. That cross-coast refactor frames today’s journey into two paradigms that power modern Vue.js apps.
Why This Choice Still Matters in 2025
Vue 3 ships both APIs to ease migration and honor developer preference, but picking one per codebase reduces cognitive load, speeds onboarding, and clarifies documentation. The Composition API unlocks stronger TypeScript inference, granular logic reuse, and tree-shakable bundle size, while the Options API shines for quick prototypes and teams comfortable with the classic mental model. Understanding their trade-offs lets you scale from solo projects in a Costa Rican surf shack to international teams shipping SaaS out of São Paulo.
The Heart of Each API
Options API — Declarative Cohesion
You describe what a component has: data
, computed
, methods
, watch
, props
, and lifecycles
. Vue sorts execution order and merges mixins under the hood.
vueCopyEdit<script>
export default {
props: { price: Number },
data() {
return { quantity: 1 };
},
computed: {
total() {
return this.price * this.quantity;
},
},
methods: {
inc() { this.quantity++; }
},
};
</script>
Composition API — Logic-First Clarity
You declare how the feature works. Reactive primitives (ref
, reactive
, computed
, watch
) combine freely inside setup()
or a <script setup>
block.
vueCopyEdit<script setup>
import { ref, computed } from 'vue';
const props = defineProps({ price: Number });
const quantity = ref(1);
const total = computed(() => props.price * quantity.value);
const inc = () => quantity.value++;
</script>
Grouping by feature—rather than by option—means state, derivatives, and side-effects live together even as components grow.
When to Reach for Composition
- Complex Components: Multiple reactive concerns (e.g., drag-and-drop, fetch, socket) stay isolated in composables instead of scattering across options.
- TypeScript Heavy Projects: Composition’s return values infer types better than the dynamic
this
of Options API. - Library Logic Reuse: Share
useCart()
oruseFetch()
composables across apps without mixin merge conflicts. - Bundle Friendly Builds: Unused imports tree-shake; Options mixins and decorations can’t.
Where Options Still Shine
- Small Prototypes: One-file components for landing pages or quick PoCs read naturally with Options.
- Learning Curve: For newcomers migrating from Vue 2 or jQuery, the structure feels declarative and safe.
- Legacy Apps: Gradual migration lets teams sprinkle Composition in new files while existing Options code remains untouched.
Hands-On Migration Walkthrough
Imagine an Options component that fetches products and filters them by search term.
Original Options Code
vueCopyEdit<script>
export default {
data() {
return { products: [], search: '' };
},
computed: {
filtered() {
return this.products.filter(p =>
p.name.toLowerCase().includes(this.search.toLowerCase())
);
},
},
mounted() {
fetch('/api/products')
.then(r => r.json())
.then(d => (this.products = d));
},
};
</script>
Composition Rewrite
vueCopyEdit<script setup>
import { ref, computed, onMounted } from 'vue';
const products = ref([]);
const search = ref('');
const filtered = computed(() =>
products.value.filter(p =>
p.name.toLowerCase().includes(search.value.toLowerCase())
)
);
onMounted(async () => {
products.value = await (await fetch('/api/products')).json();
});
</script>
Benefits Observed
Grouped concerns—fetch logic, state, derived data—in one chunk. Adding debounce, pagination, or error handling happens beside the fetch, not in remote lifecycle hooks.
Extracting a Reusable Composable
tsCopyEdit// composables/useProducts.ts
import { ref, computed, onMounted } from 'vue';
export function useProducts() {
const list = ref<Product[]>([]);
const search = ref('');
const filtered = computed(() =>
list.value.filter(p =>
p.name.toLowerCase().includes(search.value.toLowerCase())
)
);
onMounted(async () => {
list.value = await (await fetch('/api/products')).json();
});
return { list, search, filtered };
}
Consume it anywhere:
vueCopyEdit<script setup>
import { useProducts } from '@/composables/useProducts';
const { filtered, search } = useProducts();
</script>
Options API could achieve similar reuse via mixins, but composables avoid name collisions, support generics, and tree-shake when not imported.
Remote-Work Insight Box
Two months back in Panamá, our design team hot-swapped product-card styles. With Options, remapping prop names across twenty components took three PRs. Using Composition, we wrapped useCardStyle()
to supply classes and sizes; design switched palettes via one function, merged once, and islands of code remained untouched—velocity wins that matter when team members review after dinner in Mexico.
Pitfalls to Dodge
Scenario | Sneaky Issue | Quick Fix |
---|---|---|
Mixing APIs in one file | Confusing this scope | Stick to one API per component |
Reactive object lost | Assigning new value to reactive proxy | Use Object.assign or ref for primitives |
Circular imports in composables | Hidden state coupling | Extract base util or rely on injection |
Over-composing | Ten tiny composables per feature | Merge related logic to keep call sites readable |
CLI Shortcuts and Ecosystem Helpers
CLI / Lib | What it does |
---|---|
npm init vue@latest | Scaffolds Vue 3 project with Composition API + TS |
vue add vue-i18n | i18n plugin integrates with both APIs |
vite-plugin-inspect | Visualize reactivity graph for composables |
eslint-plugin-vue | Rules to forbid mixing Options & Composition |
Performance & Accessibility Checkpoints
- Bundle Inspect:
vite build --report
confirms composables tree-shake. - DevTools Vue Tab: Watch reactive graph; avoid watchers that fire thrice per second.
- Keyboard Nav: Ensure dynamic components maintain focus order regardless of API; Composition’s render function patterns can break ARIA if careless.
- Hydration:
<script setup>
SSR hydration costs less because unused options don’t initialize.
Diagram Idea (textual)
Tree comparing a 300-line Options component sliced across five option blocks versus a Composition refactor with grouped feature islands—highlighting flow of ref
and computed
nodes.
Wrap-Up
Composition and Options aren’t rivals—they’re siblings. Options remains a gentle on-ramp, while Composition unlocks scale, reuse, and type safety. Pick one intentionally, document team conventions, and stick with it. Your future self—and that intern picking up code on a shaky hostel Wi-Fi in Costa Rica—will thank you.
Got a Vue-API tale or question? Drop it below; I’ll reply between layovers and late-night arepa sessions.