Composables
|
This section documents the current Vue 3.x release line as published at
the official Vue.js documentation, which is the reference these pages are written and
verified against. No specific patch version is pinned. The Composition API with This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since Vue and its ecosystem iterate quickly. This section’s bibliography lists the reference material consulted while preparing these pages. |
A composable is a function that uses Vue’s Composition API to package and reuse stateful logic. By
convention its name starts with use, it is called from setup (or the top level of <script setup>), and
the reactive state and lifecycle hooks it creates belong to the component that called it.
What a composable is
A composable creates some reactive state, wires up whatever side effects it needs, and returns the state for the caller to use in its template or logic. The canonical first example tracks the mouse position:
// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
<script setup>
import { useMouse } from './composables/useMouse.js'
const { x, y } = useMouse()
</script>
<template>
<p>Mouse at {{ x }}, {{ y }}</p>
</template>
Each component that calls useMouse() gets its own independent x and y, and the listener is removed when
that component unmounts. This is the whole idea, laid out in
the Composables guide.
Naming and what to return
-
Name it
use<Thing>. The prefix is how a reader (and tooling such as the ESLint plugin) recognises a function that may call reactivity and lifecycle APIs. -
Return refs, not a
reactive()wrapper. Returning a plain object of refs —\{ x, y }— lets the caller destructure while keeping each property reactive. Destructuring areactive()object breaks reactivity. If you preferstate.xaccess at the call site, return a singlereactive()object instead, but do not do both. -
Expose methods alongside state when the caller needs to trigger changes:
return \{ data, error, refetch }.
Accepting reactive arguments
A composable is more reusable when its input can be a plain value, a ref, or a getter. Normalise the input
with toValue(), and read it inside a
watchEffect so the composable re-runs when a reactive argument changes:
// composables/useFetch.js
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
watchEffect(async () => {
data.value = null
error.value = null
try {
const res = await fetch(toValue(url)) // string | Ref<string> | (() => string)
data.value = await res.json()
} catch (e) {
error.value = e
}
})
return { data, error }
}
<script setup>
import { ref } from 'vue'
import { useFetch } from './composables/useFetch.js'
const id = ref(1)
// re-fetches whenever `id` changes, because the getter is read inside watchEffect
const { data, error } = useFetch(() => `/api/items/${id.value}`)
</script>
The matching TypeScript type for such a parameter is MaybeRefOrGetter<T>, and toValue is its
accessor — see TypeScript with Composition API.
Cleaning up side effects
Anything a composable starts, it must stop, so that a component using it can mount and unmount repeatedly without leaks.
-
Register teardown for the component’s lifetime with
onUnmounted(asuseMousedoes above). -
Clean up per run of a watcher with
onWatcherCleanup(Vue 3.5+), or theonCleanupargument passed towatch/watchEffectcallbacks. This runs before the next invocation and on stop:
import { ref, watch, onWatcherCleanup } from 'vue'
export function useItem(id) {
const item = ref(null)
watch(id, async (newId) => {
const controller = new AbortController()
onWatcherCleanup(() => controller.abort()) // cancel the in-flight request
const res = await fetch(`/api/items/${newId}`, { signal: controller.signal })
item.value = await res.json()
}, { immediate: true })
return { item }
}
The call-synchronously rule
A composable must be called synchronously, at the top level of setup or <script setup>, or from
inside another composable. Do not call one after an await, in an event handler, or conditionally. Vue
relies on the currently active component instance to attach lifecycle hooks and injected values, and that
instance is only available during the synchronous execution of setup. The same restriction applies to
onMounted, inject, and friends. See
Usage restrictions.
Worked example: useLocalStorage
Read once on setup, then persist on every change:
// composables/useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, initialValue) {
const raw = localStorage.getItem(key)
const state = ref(raw ? JSON.parse(raw) : initialValue)
watch(state, (value) => {
localStorage.setItem(key, JSON.stringify(value))
}, { deep: true })
return state
}
<script setup>
import { useLocalStorage } from './composables/useLocalStorage.js'
const theme = useLocalStorage('theme', 'light')
</script>
<template>
<button @click="theme = theme === 'light' ? 'dark' : 'light'">
Theme: {{ theme }}
</button>
</template>
In production, a library such as VueUse ships hardened versions of all three of these
(useMouse, useFetch, useLocalStorage) with SSR guards and shared listeners.
Composables can compose
A composable is a plain function, so one composable calls another with no ceremony and no instance overhead:
Composables vs. mixins vs. renderless components
| Approach | Trade-off |
|---|---|
Composable |
Plain function calls. Explicit inputs and outputs, no naming collisions, tree-shakeable, no component instance. The recommended way to share stateful logic. |
Mixin (Options API) |
Merges |
Renderless component |
A component that owns logic and exposes it through a scoped slot while rendering nothing. Works, but costs a component instance and only functions inside a template. Use it only when the template itself must drive the slot content. See Renderless Components. |
A composable still works from an Options-API component — call it in setup() and return what you need
(shown here only as a contrast; new code uses <script setup>):
import { useMouse } from './composables/useMouse.js'
export default {
setup() {
const { x, y } = useMouse()
return { x, y }
}
}
See also
-
Custom Directives and Plugins — the other reuse mechanisms: DOM-level directives and app-level plugins, plus the full "prefer composables over mixins" rationale.
-
Async Components and KeepAlive, Teleport, and Suspense — pairing an async
useFetch-style composable with<Suspense>. -
JavaScript Development — the closures and module semantics composables are built on.