Composables are plain functions that use Vue's reactivity. They let you write logic once, such as fetching data, tracking the mouse or handling a form, and reuse it in any component. Here are the patterns I use most.

The basic shape#

A composable starts with use, creates reactive state, and returns what the component needs:

import { ref } from 'vue'

export function useCounter(start = 0) {
  const count = ref(start)
  const increment = () => count.value++
  const reset = () => (count.value = start)

  return { count, increment, reset }
}

Clean up after yourself#

When a composable adds event listeners or timers, remove them when the component unmounts:

import { ref, onMounted, onUnmounted } from 'vue'

export function useWindowWidth() {
  const width = ref(0)
  const update = () => (width.value = window.innerWidth)

  onMounted(() => {
    update()
    window.addEventListener('resize', update)
  })
  onUnmounted(() => window.removeEventListener('resize', update))

  return { width }
}

Accept refs as input#

Use toValue() so callers can pass a plain value, a ref or a getter, and watch it to react to changes:

import { ref, toValue, watchEffect } from 'vue'

export function useUser(id) {
  const user = ref(null)

  watchEffect(async () => {
    user.value = await fetch(`/api/users/${toValue(id)}`).then(r => r.json())
  })

  return { user }
}

Shared state#

State created inside the function is new for every component. To share it, create the state outside the function. In Nuxt, prefer useState, which is also safe during server rendering.

Tips#

  • Keep one job per composable.
  • Return refs, not reactive() objects, so destructuring keeps reactivity.
  • Put them in a composables/ folder; Nuxt imports them automatically.
  • Test them like normal functions, without mounting a component.