Stop Scrolling: Organizing Complex Vue Components with Composables
Stop fighting with massive Vue components. Learn how to use the Composition API and Composables to organize logic by feature rather than by option type.
10 Jan 2026, 08:46 UTC

The 'Giant Component' Problem
As a Vue component grows, the Options API often forces a fragmented structure. You define a variable in data, a transformation in computed, and the logic to update it in methods. When a component handles three or four distinct features—like a search filter, a pagination system, and a data table—you spend more time scrolling up and down the file to connect related logic than you do actually writing code.
The solution is the Composition API and the use of Composables. Instead of organizing by option type, you organize by logical concern. This allows you to extract stateful logic into standalone functions that can be reused across your entire application.
What Exactly is a Composable?
A composable is a function that leverages Vue's reactivity system to encapsulate and return state. By convention, these functions start with "use", such as useAuth or useWindowSize. Unlike a simple utility function that takes an input and returns a value, a composable manages its own reactive state using ref or reactive.
In Vue 3, the <script setup> syntax is the standard entry point. It eliminates the need to manually return variables to the template, making the transition from Options API to Composition API much leaner.
Practical Example: A Reusable Fetch Composable
Consider a scenario where multiple components need to fetch data from an API and track loading and error states. Instead of repeating this logic in every component, you can create a composable.
// useFetch.js
import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
const execute = async () => {
loading.value = true
error.value = null
try {
const response = await fetch(url)
if (!response.ok) throw new Error('Network response was not ok')
data.value = await response.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
return { data, error, loading, execute }
}
To use this in a component, import the function and destructure the reactive properties. Run this in your component's <script setup> block:
<script setup>
import { onMounted } from 'vue'
import { useFetch } from './composables/useFetch'
const { data, error, loading, execute } = useFetch('https://api.example.com/items')
// Lifecycle hooks are called synchronously in setup
onMounted(() => {
execute()
})
</script>
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<ul v-else>
<li v-for="item in data" :key="item.id">{{ item.name }}</li>
</ul>
</template>
The Reactivity Trap: Ref vs. Reactive
One of the most common points of failure when moving to the Composition API is the choice between ref and reactive. While reactive is useful for grouping related state into an object, it has a critical limitation: destructuring.
| Feature | ref() | reactive() |
|---|---|---|
| Type | Any value (primitive or object) | Objects/Arrays only |
| Access | Requires .value in JS |
Direct access |
| Destructuring | Preserves reactivity | Loses reactivity (unless using toRefs) |
If you return a reactive object from a composable and destructure it in your component (e.g., const { count } = useCounter()), the count variable becomes a plain number and will no longer trigger template updates. For this reason, using ref for individual state pieces is generally the safer, more predictable default.
Trade-offs and Limitations
While composables solve the "giant component" problem, they introduce the risk of fragmentation. Over-extracting logic into dozens of tiny files can make the data flow harder to trace. If a piece of logic is only used in one component and is less than 50 lines, keeping it inside the component's <script setup> is often more readable than creating a separate file.
Verification Check
To verify your composable is working correctly, check the Vue DevTools. The state returned by a composable should appear as a Ref in the component's setup state. If you see a static value that doesn't change when the API call completes, you likely encountered the destructuring issue mentioned above.
Moving Forward
Start by identifying the most repetitive logic in your current components—usually API calls, form validation, or window event listeners. Extract one of these into a composable using ref, and move it into a /composables directory. This shift doesn't just clean up your files; it creates a library of internal tools that makes building new features significantly faster.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.