What a Composable Is
A composable is a plain function that uses Vue's reactivity APIs and is called while a component sets up.
export function useThing(args) { /* refs, computed, hooks */ return { /* refs and functions */ } }A composable is a plain function that uses Vue's reactivity APIs — ref, computed, watch, the lifecycle hooks — and is called from a component's <script setup>. It exists so stateful logic can live outside a component and be used by several of them. Nothing is registered and nothing is installed: you export a function, you import it, you call it.
The naming convention
The name starts with use and is camelCase: useMouse, useFilteredItems. Vue does not enforce the prefix. It is a signal to the reader that the function creates reactive state or registers hooks, so calling it anywhere other than during setup is a mistake. A function that only transforms its arguments — a date formatter, a currency helper — is a utility, not a composable, and does not take the prefix.
What it returns
Return an object of refs and functions, not unwrapped numbers and strings. The caller destructures the object and each ref keeps working, because a ref is an object whose value is read through .value. Templates unwrap refs for you, so {{ x }} reads the current value.
What trips people up
The call has to happen synchronously while the component is setting up. Vue uses the currently active component instance to attach lifecycle hooks and to dispose watchers on unmount. A composable called inside an event handler or a setTimeout registers nothing, and its watchers are never cleaned up.
Example
<script setup lang="ts">
import { useMouse } from '../composables/useMouse'
const { x, y } = useMouse()
</script>
<template>
<p>Pointer at {{ x }}, {{ y }}</p>
</template>
<style scoped>
p {
font-variant-numeric: tabular-nums;
}
</style>When to use it
- Share mouse, scroll or window-size tracking between several components without repeating the listener code.
- Lift two unrelated jobs out of one overgrown component so each has its own file and its own name.
- Give a feature's state a testable home that does not need a component mounted around it.
More examples
The composable itself
Two refs, one DOM listener, and the removal written next to the addition. The component that calls this gets live coordinates and never has to remember to tear the listener down. Note the writes use `.value`: only templates unwrap a ref.
// src/composables/useMouse.ts
import { onMounted, onUnmounted, ref } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event: MouseEvent) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}A helper is not a composable
No refs, no hooks, no dependency on a component instance. It is a pure function, so it keeps a plain name and can be called from anywhere, including a test. Reserve the `use` prefix for functions that touch reactivity.
// src/lib/formatPrice.ts
export function formatPrice(cents: number): string {
return (cents / 100).toFixed(2)
}
console.log(formatPrice(1999))
// Output: 19.99Each call owns its state
Calling a composable twice produces two independent sets of refs. That is usually what you want: two components, two counters. It also means this composable can be called in a test, because it registers no lifecycle hooks and so needs no component instance.
// src/composables/useCounter.ts
import { ref } from 'vue'
export function useCounter(initial = 0) {
const count = ref(initial)
function increment() {
count.value += 1
}
return { count, increment }
}
const a = useCounter(0)
const b = useCounter(10)
a.increment()
console.log(a.count.value, b.count.value)
// Output: 1 10
Discussion