Composables are functions that encapsulate reactive state and logic for reuse across components. They follow the use naming convention.

Why Composables?

  • Share logic without mixins or inheritance
  • Keep components focused on UI
  • Test business logic independently
  • Compose small functions into larger behaviors

Basic Composable

composables/useCounter.js:

  import { ref, computed } from 'vue';

export function useCounter(initial = 0) {
  const count = ref(initial);
  const double = computed(() => count.value * 2);

  function increment() {
    count.value++;
  }

  function decrement() {
    count.value--;
  }

  function reset() {
    count.value = initial;
  }

  return { count, double, increment, decrement, reset };
}
  

Use in a component:

  <script setup>
import { useCounter } from '../composables/useCounter';

const { count, double, increment, decrement } = useCounter(10);
</script>

<template>
  <p>{{ count }} (×2 = {{ double }})</p>
  <button @click="increment">+</button>
  <button @click="decrement">−</button>
</template>
  

useLocalStorage Composable

  import { ref, watch } from 'vue';

export function useLocalStorage(key, defaultValue) {
  const stored = localStorage.getItem(key);
  const value = ref(stored ? JSON.parse(stored) : defaultValue);

  watch(value, (newVal) => {
    localStorage.setItem(key, JSON.stringify(newVal));
  }, { deep: true });

  return value;
}
  
  <script setup>
import { useLocalStorage } from '../composables/useLocalStorage';

const theme = useLocalStorage('theme', 'light');
</script>
  

useMousePosition Composable

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

export function useMousePosition() {
  const x = ref(0);
  const y = ref(0);

  function update(event) {
    x.value = event.clientX;
    y.value = event.clientY;
  }

  onMounted(() => window.addEventListener('mousemove', update));
  onUnmounted(() => window.removeEventListener('mousemove', update));

  return { x, y };
}
  

Composing Composables

Combine smaller composables into larger ones:

  import { computed } from 'vue';
import { useMousePosition } from './useMousePosition';

export function useDistanceFromCenter() {
  const { x, y } = useMousePosition();
  const centerX = window.innerWidth / 2;
  const centerY = window.innerHeight / 2;

  const distance = computed(() =>
    Math.hypot(x.value - centerX, y.value - centerY)
  );

  return { x, y, distance };
}
  

Conventions

  1. Name with useuseFetch, useAuth, useToggle
  2. Return refs — so destructuring stays reactive (or document when to use storeToRefs-style patterns)
  3. Call at top level — only invoke composables inside setup or other composables
  4. Cleanup in onUnmounted — remove listeners, abort requests

Use composables for logic; use Pinia when multiple unrelated components need the same global state.

Next: fetch data from APIs with async patterns.