Extract and reuse stateful logic across your app.
1What is a Composable?
In the context of Vue applications, a 'composable' is a function that leverages Vue's Composition API to encapsulate and reuse stateful logic. If you are familiar with React, this is identical in concept to a 'Custom Hook'.
2Conventions
Composables are standard JavaScript/TypeScript files (not .vue files). By convention, their names always start with use, such as useFetch or useDarkTheme. They typically return an object containing refs or functions.
3Why not just classes/mixins?
Vue 2 relied on Mixins for logic reuse, but mixins caused naming collisions and made it hard to trace where a variable came from. Composables use explicit imports and destructuring (const { x } = useMouse()), making the source of every variable 100% clear.
4Step-by-Step Breakdown
What happens when you have logic (like fetching user data, or tracking mouse position) that you want to reuse across MULTIPLE components?
In Vue 3, the answer is "Composables". A Composable is simply a standard JavaScript function that leverages Vue's Composition API (ref, onMounted, etc.).
What do we call a reusable JavaScript function that encapsulates stateful logic using Vue's Composition API (like ref and watch)?
- →A Mixin
- →A Component
- →A Composable
By convention, the names of Composables always start with "use". For example, useFetch, useMouse, useTheme.
Inside the composable, you can use lifecycle hooks just like you would inside a component!
Is it possible to use Vue lifecycle hooks like onMounted and onUnmounted inside a standard JavaScript function (Composable) outside of a .vue file?
- →No, hooks only work in .vue files
- →Yes, as long as it's called synchronously inside setup()
To use the Composable in your component, you just import it and call it inside <script setup>. The component gets the reactive variables!
Composables are the modern replacement for Vue 2 "Mixins". They solve all the problems of mixins (naming collisions, hidden sources of data).
What Vue 2 feature are Composables designed to replace in Vue 3?
- →Directives
- →Mixins
- →Filters
The Vue community has created massive libraries of pre-built composables, like VueUse, which gives you hundreds of ready-to-use functions.
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Composables That Manage Focus
A composable like useFocusTrap(containerRef) is the idiomatic Vue way to move keyboard focus into a modal and restore it on close, which keyboard and screen-reader users depend on — accessibility logic like this belongs in a dedicated, reusable composable rather than duplicated onMounted blocks in every dialog component.
SEO Implications
- 1
Composables Run Client-Side Only
Composables that call onMounted, touch window, or fetch data only in the browser never execute during Vue's server-side rendering pass, so any content they produce is invisible in the initial HTML response search engines fetch; content that must be indexable should be fetched through an SSR-aware data layer instead of a composable that only runs after mount.
Best Practices
Return Refs, Not Raw Values
A composable should return reactive refs (const { x, y } = useMouse()) rather than plain destructured numbers, otherwise the component loses reactivity the moment it destructures the returned object.
Accept MaybeRef Arguments
Accept both a plain value and a ref for composable parameters (useFetch(url: MaybeRef<string>)) and unwrap it internally with toValue(), so callers can pass either a static URL or a reactive one that triggers a re-fetch when it changes.
Frequent Bugs
Calling a lifecycle hook like onMounted() inside a composable after an await.
onMounted, onUnmounted, and similar hooks must be called synchronously during the component's setup() call stack. Code like `await fetchConfig(); onMounted(...)` runs onMounted after Vue has lost track of the active component instance, so it silently no-ops — resolve the await inside the component instead, or fire the async work from inside the hook.
Real-World Examples
useLocalStorage Composable
A useLocalStorage(key, defaultValue) composable shared by a settings page and a cart page keeps a ref synced to localStorage.setItem on every change and re-hydrates it on mount, giving both features persistent state without either component knowing localStorage exists.
export function useLocalStorage(key, initial) {
const data = ref(JSON.parse(localStorage.getItem(key)) ?? initial);
watch(data, (v) => localStorage.setItem(key, JSON.stringify(v)), { deep: true });
return data;
}