Vue 3's Composition API has become the go-to for building modern components—but that doesn't mean every composable is good by default. At Vue.js Amsterdam 2025, Alexander Lichter shared what makes composables clean, predictable, and reusable.

Follow @TheAlexLichter for more Vue insight. Check out the DejaVue podcast

This post summarizes the key takeaways from his talk: "The Composable Handbook: Stop Writing Bad Composables".


Why Composables?

The Options API still works, but the Composition API gives you:

  • Future-proof design (e.g., upcoming Vapor)
  • Stronger TypeScript support
  • Cleaner code-sharing (hooks, watchers, and reactive state in one place)
  • Better separation of concerns
  • A more "JavaScript-like" structure

Downside: without patterns, Composition API can be harder to get right.


What is a Composable?

"A composable is a function that uses Vue's Composition API to encapsulate and reuse stateful logic."

A function is a composable if it:

  • Uses another composable
  • Uses lifecycle hooks (onMounted, onUnmounted, etc.)
  • Uses the Reactivity API (ref, reactive, computed)
  • Uses dependency injection (provide / inject)

If none of those apply, it's probably just a plain old utility function.


Organizing Your Composables

Think like a vanilla JS dev:

  • Colocate logic by concern or feature
  • Encapsulate logic in functions
  • Compose those into more complex behavior
  • Build layers of abstraction

Even inline composables (functions declared inside components) are fine when scoped properly.


5 Rules for Better Composables

1. Don't Reinvent the Wheel

Use VueUse—it has over 200 proven composables.

2. Don't Make Composables Async

You can await them in your component, but not inside another composable. Otherwise, you risk losing lifecycle context.

3. Don't Call Them in the Wrong Place

Composables must be called at the top level of:

  • setup()
  • another composable

Don't use them in:

  • event handlers
  • inside conditionals
  • on the server

4. Use readonly()

Prevent consumers from mutating internal state directly:

return {
  user: readonly(user),
  fetchUser
}

5. Avoid Global Refs in SSR

Instead of this (bad for SSR):

const user = ref(null)

Use this (Nuxt example):

const user = useState(() => null)

Next Steps

Good composables are predictable, reusable, and often boring—and that's a good thing.