Skip to main content

Command Palette

Search for a command to run...

Understanding Composables in Vue 3

Updated
6 min read
Understanding Composables in Vue 3
G

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.