What are Composables?

Composables are reusable functions that leverage Vue's Composition API to encapsulate and reuse stateful logic.

Key Concept: Composables are similar to React Hooks but more flexible. They can be called anywhere, not just at the top level.

Why Use Composables?

Code Reuse

Share logic across multiple components without duplication

Organization

Group related logic together instead of scattering it across options

Type Safety

Better TypeScript inference compared to mixins

Core Reactivity APIs

ref() and reactive()

import { ref, reactive, computed, watch } from 'vue'

// ref - for primitive values
const count = ref(0)
const message = ref('Hello')

// Access value with .value
console.log(count.value) // 0
count.value++

// reactive - for objects
const state = reactive({
  user: {
    name: 'John',
    age: 30
  },
  items: []
})

// Access directly (no .value)
console.log(state.user.name)
state.user.age++

computed() and watch()

import { ref, computed, watch } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// Computed property
const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`
})

// Watcher
watch(fullName, (newValue, oldValue) => {
  console.log(`Name changed from ${oldValue} to ${newValue}`)
})

// Watch multiple sources
watch([firstName, lastName], ([newFirst, newLast]) => {
  console.log(`First: ${newFirst}, Last: ${newLast}`)
})

// Watch with options
watch(
  () => state.user,
  (newUser) => {
    console.log('User changed:', newUser)
  },
  { deep: true, immediate: true }
)

watchEffect()

import { ref, watchEffect } from 'vue'

const count = ref(0)
const doubled = ref(0)

// Automatically tracks dependencies
watchEffect(() => {
  doubled.value = count.value * 2
  console.log(`Count: ${count.value}, Doubled: ${doubled.value}`)
})

// Runs immediately and re-runs when count changes
count.value++ // Logs: "Count: 1, Doubled: 2"

Creating Custom Composables

useFetch Composable

// composables/useFetch.js
import { ref, watch } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)

  async function fetchData() {
    loading.value = true
    error.value = null

    try {
      const response = await fetch(url.value)
      data.value = await response.json()
    } catch (err) {
      error.value = err
    } finally {
      loading.value = false
    }
  }

  // Re-fetch when URL changes
  watch(url, fetchData, { immediate: true })

  return { data, error, loading, refetch: fetchData }
}

// Usage in component
<script setup>
import { ref } from 'vue'
import { useFetch } from '@/composables/useFetch'

const userId = ref(1)
const url = computed(() => `/api/users/${userId.value}`)
const { data: user, loading, error } = useFetch(url)
</script>

<template>
  <div v-if="loading">Loading...</div>
  <div v-else-if="error">Error: {{ error.message }}</div>
  <div v-else>{{ user }}</div>
</template>

useLocalStorage Composable

// composables/useLocalStorage.js
import { ref, watch } from 'vue'

export function useLocalStorage(key, defaultValue) {
  const data = ref(defaultValue)

  // Read from localStorage on init
  const stored = localStorage.getItem(key)
  if (stored) {
    data.value = JSON.parse(stored)
  }

  // Save to localStorage on change
  watch(
    data,
    (newValue) => {
      localStorage.setItem(key, JSON.stringify(newValue))
    },
    { deep: true }
  )

  return data
}

// Usage
<script setup>
import { useLocalStorage } from '@/composables/useLocalStorage'

const theme = useLocalStorage('theme', 'light')
const todos = useLocalStorage('todos', [])

function toggleTheme() {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}
</script>

useCounter Composable

// composables/useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)

  const doubled = computed(() => count.value * 2)
  const isEven = computed(() => count.value % 2 === 0)

  function increment() {
    count.value++
  }

  function decrement() {
    count.value--
  }

  function reset() {
    count.value = initialValue
  }

  return {
    count,
    doubled,
    isEven,
    increment,
    decrement,
    reset
  }
}

// Usage
<script setup>
import { useCounter } from '@/composables/useCounter'

const { count, doubled, isEven, increment, decrement, reset } = useCounter(10)
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Doubled: {{ doubled }}</p>
    <p>Is Even: {{ isEven }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="reset">Reset</button>
  </div>
</template>

useMouse Composable

// 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 }
}

// Usage
<script setup>
import { useMouse } from '@/composables/useMouse'

const { x, y } = useMouse()
</script>

<template>
  <div>Mouse position: {{ x }}, {{ y }}</div>
</template>

VueUse Library

Collection of essential Vue Composition utilities - the Swiss Army knife of Vue composables.

Installation

npm install @vueuse/core

Popular VueUse Composables

import {
  useLocalStorage,
  useMouse,
  useWindowSize,
  useOnline,
  useFetch,
  useClipboard,
  useTitle
} from '@vueuse/core'

// Browser APIs
const { x, y } = useMouse()
const { width, height } = useWindowSize()
const online = useOnline()

// LocalStorage with reactivity
const state = useLocalStorage('my-state', { count: 0 })

// Clipboard
const { text, copy, copied } = useClipboard()
copy('Hello World')

// Document title
const title = useTitle()
title.value = 'New Page Title'

// Fetch with auto-refetch
const { data } = useFetch('/api/users').json()

VueUse Categories

Category Examples
State useLocalStorage, useSessionStorage, useStorage
Sensors useMouse, useDeviceOrientation, useGeolocation
Browser useClipboard, useTitle, useFavicon
Network useFetch, useOnline, useWebSocket
Animation useTransition, useInterval, useTimeout
Component useVModel, useSlots, useTemplateRef
Learn more: Visit vueuse.org for complete documentation and 200+ composables.

Best Practices

Do's
  • Name composables with "use" prefix
  • Return reactive objects or refs
  • Make composables flexible with parameters
  • Clean up side effects in onUnmounted
  • Use TypeScript for better type inference
  • Keep composables focused and single-purpose
  • Document parameters and return values
Don'ts
  • Don't mutate external state directly
  • Don't forget to clean up event listeners
  • Don't mix reactive and non-reactive returns
  • Don't create composables that are too generic
  • Don't ignore memory leaks
  • Don't use composables outside setup()
  • Don't over-engineer simple logic

Composable Naming Convention

// Good naming
useFetch()
useLocalStorage()
useCounter()
useMouse()

// Bad naming
fetch()          // Missing "use" prefix
handleMouse()    // Sounds like a function, not a composable
mousePosition()  // Not clear it's a composable

Return Value Pattern

// Good - Return object for flexibility
export function useCounter() {
  const count = ref(0)
  const increment = () => count.value++

  return { count, increment }
}

// Good - Can destructure what you need
const { count } = useCounter()
const { increment } = useCounter()

// Also good - Return refs for simple composables
export function useCount() {
  return ref(0)
}