β¨Vue 3 Composition API: Building Scalable, Maintainable Components


Hook:
βWhy Vue's Composition API is changing how we think about component architecture in 2025.β π₯
If youβve worked on a growing Vue app, you know the struggle: Options API works fine for tiny components, but big components become messy spaghetti π. The Composition API changes everything by letting you group logic by feature, making components cleaner, reusable, and easier to test.
1. Quick Before/After Example π
Before (Options API β messy π ):
<template>
<div>
<input v-model="name" />
<button @click="submitForm">Submit</button>
</div>
</template>
<script>
export default {
data() { return { name: '', loading: false }; },
methods: {
submitForm() {
this.loading = true;
fetch('/api/submit', { method: 'POST', body: JSON.stringify({ name: this.name }) })
.finally(() => (this.loading = false));
}
}
};
</script>
After (Composition API β clean & reusable β¨):
<script setup>
import { ref } from 'vue';
const name = ref('');
const loading = ref(false);
async function submitForm() {
loading.value = true;
try {
await fetch('/api/submit', { method: 'POST', body: JSON.stringify({ name: name.value }) });
} finally {
loading.value = false;
}
}
</script>
<template>
<div>
<input v-model="name" />
<button @click="submitForm" :disabled="loading">Submit</button>
</div>
</template>
Why this rocks: πΈ
Logic centralized in
setup()
Explicit reactive state (
ref()
)Ready for reusable composables
2. Composition API Fundamentals π
Core tools:
setup()
: Entry point for component logicref()
vsreactive()
: Primitives vs objects/arrayscomputed()
: Derived reactive valueswatch()
: React to state changesLifecycle Hooks:
onMounted()
,onUpdated()
,onUnmounted()
TypeScript Example (strong typing π‘οΈ):
<script setup lang="ts">
import { ref, computed } from 'vue';
const count = ref<number>(0);
const doubleCount = computed<number>(() => count.value * 2);
function increment(): void { count.value++; }
</script>
β Benefit: Better autocompletion, fewer runtime errors, and maintainable code.
3. Real-World Composables π§
A. useFetch (with full lifecycle + error handling):
import { ref, onMounted } from 'vue';
export function useFetch<T>(url: string) {
const data = ref<T | null>(null);
const loading = ref(false);
const error = ref<string | null>(null);
const fetchData = async () => {
loading.value = true;
error.value = null;
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`Error! Status: ${res.status}`);
data.value = await res.json();
} catch (err: any) {
error.value = err.message;
} finally {
loading.value = false;
}
};
onMounted(fetchData);
return { data, loading, error, fetchData };
}
B. useLocalStorage:
import { ref, watch } from 'vue';
export function useLocalStorage<T>(key: string, initialValue: T) {
const stored = localStorage.getItem(key);
const data = ref<T>(stored ? JSON.parse(stored) : initialValue);
watch(data, (val) => localStorage.setItem(key, JSON.stringify(val)), { deep: true });
return data;
}
C. useDebounce:
import { ref, watch } from 'vue';
export function useDebounce<T>(value: T, delay = 300) {
const debounced = ref(value);
let timer: ReturnType<typeof setTimeout>;
watch(() => value, (val) => {
clearTimeout(timer);
timer = setTimeout(() => debounced.value = val, delay);
});
return debounced;
}
D. useEventListener:
import { onMounted, onUnmounted } from 'vue';
export function useEventListener(target: EventTarget, event: string, handler: EventListener) {
onMounted(() => target.addEventListener(event, handler));
onUnmounted(() => target.removeEventListener(event, handler));
}
β Why Composables Are Awesome:
Reusable across components
Easier to test
Keeps
setup()
clean and readable
4. Advanced Patterns π
Pinia Store Example:
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useUserStore = defineStore('user', () => {
const user = ref({ name: '', email: '' });
function updateUser(newUser: { name: string; email: string }) { user.value = newUser; }
return { user, updateUser };
});
Lifecycle Hooks in Composables:
import { ref, onMounted, onUnmounted } from 'vue';
export function useTimer() {
const time = ref(0);
let interval: ReturnType<typeof setInterval>;
onMounted(() => { interval = setInterval(() => time.value++, 1000); });
onUnmounted(() => clearInterval(interval));
return { time };
}
Pro Tips for Juniors:
Keep composables focused
Clean up side effects
Donβt overuse
reactive()
for primitives
5. Complete Jest Test Example π§ͺ
import { useLocalStorage } from './useLocalStorage';
import { nextTick } from 'vue';
test('useLocalStorage stores and updates value', async () => {
const storageKey = 'username';
localStorage.clear();
const username = useLocalStorage(storageKey, 'John');
expect(username.value).toBe('John');
username.value = 'Alice';
await nextTick();
expect(JSON.parse(localStorage.getItem(storageKey)!)).toBe('Alice');
});
6. Migration Strategy π§
Checklist for Teams:
Identify complex components
Start small β refactor components with reusable logic
Gradually introduce TypeScript
Test all composables
Monitor performance & bundle size
Capability Matrix:
Component Size | Options API | Composition API |
Small/simple | β Optional | Optional |
Medium | βοΈ | Recommended |
Large/complex | β | β |
7. Common Pitfalls & Quick Fixes β οΈ
Pitfall | Fix |
Overusing reactive() for primitives | Use ref() instead |
Forgetting to clean up side effects | Use onUnmounted() |
Creating too many tiny composables | Group related logic together |
Not typing reactive state in TS | Add proper TypeScript types |
Using composables incorrectly in Options API | Refactor to setup() |
8. Performance & Testing β‘
Tree-shakable composables β smaller bundle size
Modular logic β fewer unnecessary re-renders
Testable composables β easier unit tests
9. Real Project Insights π‘
Teams see faster onboarding for new developers
Reusable composables reduce duplicate logic by ~50%
Migration may take weeks for large apps, but results in cleaner, maintainable architecture
10. Conclusion & Next Steps π―
Composition API = scalable, maintainable, reusable, testable components
Feature-based logic grouping > Options API type-based grouping
Recommended for juniors wanting clean, modern Vue apps
Next Step: Refactor one small component, create a couple of composables, and see the magic happen β¨
Subscribe to my newsletter
Read articles from Deepa Elango directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
