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 <script setup> is the authoring style used throughout; the Options API is shown only as a contrast.

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 a reactive() object breaks reactivity. If you prefer state.x access at the call site, return a single reactive() 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 (as useMouse does above).

  • Clean up per run of a watcher with onWatcherCleanup (Vue 3.5+), or the onCleanup argument passed to watch / watchEffect callbacks. 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:

flowchart TD A["UserPanel -- script setup"] --> B["useUserFeed(id)"] B --> C["useFetch(url getter)"] C --> D["watchEffect + toValue(url)"] B --> E["onUnmounted / onWatcherCleanup"]

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 data / methods / hooks into a component. The property source is invisible at the use site, two mixins can silently collide on a key, and there is no way to pass arguments. See Composables vs. Mixins.

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