Moving Beyond the Options API: Organizing Vue 3 Logic with <script setup>
Stop fighting fragmented logic in Vue components. Learn how <script setup> and the Composition API allow you to organize code by feature rather than option type.
05 Feb 2026, 21:26 UTC

The 'Option' Bottleneck
In the Vue 2 era, the Options API provided a clear structure: data goes in data(), methods go in methods, and computed properties go in computed. This works perfectly for small components. However, as a component grows to handle multiple features—such as a search bar, a pagination system, and a data table all in one view—the logic for a single feature becomes fragmented. You end up scrolling up and down the file to connect a data property to its corresponding method.
The takeaway is simple: the Composition API, specifically using <script setup>, allows you to group code by logical concern rather than by option type. This reduces cognitive load and makes logic reuse significantly more predictable.
Reactivity Primitives: Ref vs. Reactive
At the heart of the Composition API are reactivity primitives. Choosing between ref and reactive is the first major decision you'll make in a component.
- ref(): Used for primitives (strings, booleans, numbers) or objects. It wraps the value in an object with a single
.valueproperty. In the<template>, Vue automatically unwraps these, so you don't need to write.value. - reactive(): Used exclusively for objects or arrays. It creates a reactive proxy of the object. While it feels more like a standard JavaScript object, it has a critical limitation: if you destructure a reactive object, you lose reactivity.
For most use cases, ref is the safer default because it provides a clear visual indicator (the .value property) that the variable is reactive, and it cannot be accidentally "broken" by destructuring.
Implementing a Composable
The most powerful pattern in Vue 3 is the Composable. A composable is a function that leverages the Composition API to encapsulate and reuse stateful logic. This replaces the old "Mixins" pattern, which often suffered from naming collisions and "mystery" data sources.
Below is an example of a useCounter composable and its implementation in a component using <script setup>.
1. The Composable (src/composables/useCounter.js)
import { ref, computed } from 'vue';
export function useCounter(initialValue = 0) {
const count = ref(initialValue);
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
return {
count,
doubleCount,
increment
};
}
2. The Component (src/components/CounterView.vue)
<script setup>
import { useCounter } from '@/composables/useCounter';
// Initialize the composable
const { count, doubleCount, increment } = useCounter(10);
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double: {{ doubleCount }}</p>
<button @click="increment">Add 1</button>
</div>
</template>
The Cost of Flexibility: Common Pitfalls
While <script setup> removes boilerplate (like the need to return variables to the template), it introduces a few engineering risks:
- Destructuring Loss: If you use
reactive()for a state object and try to return it from a composable viaconst { x } = state,xbecomes a plain variable and will no longer trigger UI updates. UsetoRefs()to convert reactive properties into individual refs before destructuring. - Computed vs. Watch: Developers often use
watchEffectto update a value based on another. This is an anti-pattern. If a value can be derived from existing state, always usecomputed(). Computed properties are cached and only re-evaluate when their dependencies change, whereas watchers can trigger unnecessary side effects and render cycles. - Macro Confusion:
definePropsanddefineEmitsare compiler macros. You do not import them from 'vue'; they are processed by the Vue compiler during the build step. Attempting to import them will result in a warning or error.
Verification and Adoption
To verify your implementation, use the Vue DevTools browser extension. When using the Composition API, check the "Setup" section of the component tree. If a variable is listed as a Ref but the UI isn't updating, check if you accidentally destructured a reactive object without toRefs.
For teams migrating from Vue 2 or the Options API, there is no need for a "big bang" rewrite. Vue 3 allows both styles to coexist. The best path forward is to use <script setup> for all new components and migrate existing ones only when they require significant feature additions.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.