Understanding Composables in Vue 3

I am a Software developer and I'm passionate about learning new things.
One of the most powerful ideas introduced with Vue 3 is composables.
You’ll hear people say things like “extract it into a composable” or “just use VueUse,” and at first, it can feel abstract. But composables are not magic. They’re just a clear way to organise logic that grows beyond a single component.
This article explains what composables are, when to use them, and how VueUse makes them easier, with simple examples.
What Is a Composable?
A composable is simply a function that uses Vue’s reactivity system and returns a reactive state or behaviour.
That’s it.
If you can write a function, you can write a composable.
Composables exist to solve one problem: reusing logic without cluttering components.
The Problem Composables Solve
Imagine you have multiple components that need to track the window size.
In Vue 2, this logic often ended up duplicated or awkwardly mixed into components.
In Vue 3, composables let you extract that logic into one place and reuse it cleanly.
But let’s use a more practical example.
Example: Extracting a Countdown Timer
Earlier, we built a simple countdown timer directly inside a component. It worked, but it mixed:
• Reactive state • Timer logic • Cleanup logic • UI concerns
That’s manageable when the component is small. But what if you need that countdown in multiple places?
That’s when you extract it.
Step 1: The Extracted Composable
Here’s the same countdown logic moved into a composable.
useCountdown.ts
import { ref, onUnmounted } from 'vue'
export function useCountdown(initialValue = 10) {
const time = ref(initialValue)
let timer: ReturnType<typeof setInterval> | null = null
function start() {
if (timer) return
timer = setInterval(() => {
if (time.value > 0) {
time.value--
} else {
stop()
}
}, 1000)
}
function stop() {
if (timer) {
clearInterval(timer)
timer = null
}
}
function reset() {
stop()
time.value = initialValue
}
onUnmounted(() => {
stop()
})
return {
time,
start,
stop,
reset
}
}
This composable:
• Holds reactive state • Manages side effects • Handles cleanup • Exposes a clean public API
The component no longer cares about interval logic. It just uses behavior.
Step 2: Using It Inside a Component
Now your component becomes dramatically simpler.
<script setup>
import { useCountdown } from '@/composables/useCountdown'
const { time, start, reset } = useCountdown(10)
</script>
<template>
<div>
<p>Time left: {{ time }}</p>
<button @click="start">Start</button>
<button @click="reset">Reset</button>
</div>
</template>
Notice what’s missing:
No lifecycle clutter. No interval cleanup inside the component. No mixed responsibilities.
The component handles UI. The composable handles logic.
That separation is the real power.
A Simpler Custom Composable Example
If the countdown feels too dynamic, here’s a smaller example.
useWindowWidth.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useWindowWidth() {
const width = ref(window.innerWidth)
function updateWidth() {
width.value = window.innerWidth
}
onMounted(() => {
window.addEventListener('resize', updateWidth)
})
onUnmounted(() => {
window.removeEventListener('resize', updateWidth)
})
return {
width
}
}
Again:
• Reactive state • Side effects • Cleanup • Clear return value
Same pattern. Different problem.
This Is the Core Idea
Composables let you:
• Group related logic • Reuse it across components • Keep components focused on UI • Scale behavior without chaos
Once you see this, the Composition API starts to make sense.
Enter VueUse
VueUse is a collection of ready-made composables for common problems.
Instead of writing everything from scratch, you can use well-tested composables maintained by the Vue community.
Think of VueUse as a utility library for reactivity.
Example: Window Size With VueUse
Instead of writing useWindowWidth yourself, VueUse already gives you useWindowSize.
import { useWindowSize } from '@vueuse/core'
const { width, height } = useWindowSize()
That’s it.
No event listeners. No cleanup. No boilerplate.
Example: Dark Mode With VueUse
Managing dark mode manually can get messy.
With VueUse:
import { useDark, useToggle } from '@vueuse/core'
const isDark = useDark()
const toggleDark = useToggle(isDark)
Now your logic is clear and intentional.
Example: Local Storage State
Storing reactive state in local storage is another common need.
import { useLocalStorage } from '@vueuse/core'
const username = useLocalStorage('username', '')
Reactive state that persists automatically.
When Should You Create Your Own Composable?
Create your own composable when:
• Logic is reused in multiple places • A component feels cluttered • You’re managing side effects • You want clearer separation of concerns
Use VueUse when:
• The problem is common • A composable already exists • You want battle-tested behavior
You don’t have to choose one or the other. They work best together.
A Common Mistake
Not everything needs to be a composable.
If logic is tightly coupled to a single component and unlikely to be reused, keeping it inline is fine.
Composables are about clarity, not abstraction for its own sake.
Why This Matters for Migration
For developers coming from Vue 2, composables are often the missing piece.
They explain why the Composition API exists in the first place.
Once you start extracting logic like timers, listeners, or shared behavior into composables, Vue 3 stops feeling complex and starts feeling intentional.
Final Thoughts
Composables are not an advanced feature. They’re a clarity feature.
They help you write code that’s easier to read, easier to reuse, and maintain.
And with VueUse, you don’t have to start from zero.
In the next article, I’ll show how provide and inject fit into this architecture, and how to combine them with composables for clean, scalable patterns.
If you’d like next, I can:
• Add architecture diagrams to this article • Turn this into a LinkedIn carousel with code highlights • Write the provide/inject follow-up • Or create a “Vue 3 Patterns” mini ebook outline
You’re building a very strong technical narrative now.



