Files
agent-skills/skills/vue-best-practices/reference/composable-composition-pattern.md
Jason Woltje f5792c40be feat: Complete fleet — 94 skills across 10+ domains
Pulled ALL skills from 15 source repositories:
- anthropics/skills: 16 (docs, design, MCP, testing)
- obra/superpowers: 14 (TDD, debugging, agents, planning)
- coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth)
- better-auth/skills: 5 (auth patterns)
- vercel-labs/agent-skills: 5 (React, design, Vercel)
- antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo)
- Plus 13 individual skills from various repos

Mosaic Stack is not limited to coding — the Orchestrator and
subagents serve coding, business, design, marketing, writing,
logistics, analysis, and more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:27:42 -06:00

6.8 KiB

title, impact, impactDescription, type, tags
title impact impactDescription type tags
Compose Composables for Complex Logic MEDIUM Building composables from other composables creates reusable, testable building blocks best-practice
vue3
composables
composition-api
patterns
code-organization

Compose Composables for Complex Logic

Impact: MEDIUM - Composables can (and should) call other composables. This composition pattern allows you to build complex functionality from smaller, focused, reusable pieces. Each composable handles one concern, and higher-level composables combine them.

This is one of the key advantages of the Composition API over mixins - dependencies are explicit and traceable.

Task Checklist

  • Extract reusable logic into focused, single-purpose composables
  • Build complex composables by combining simpler ones
  • Ensure each composable has a single responsibility
  • Pass data between composed composables via parameters or refs

Example: Building a Mouse Tracker from Smaller Composables

// composables/useEventListener.js - Low-level building block
import { onMounted, onUnmounted, toValue } from 'vue'

export function useEventListener(target, event, callback) {
  onMounted(() => {
    const el = toValue(target)
    el.addEventListener(event, callback)
  })

  onUnmounted(() => {
    const el = toValue(target)
    el.removeEventListener(event, callback)
  })
}

// composables/useMouse.js - Composes useEventListener
import { ref } from 'vue'
import { useEventListener } from './useEventListener'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  // Reuse the event listener composable
  useEventListener(window, 'mousemove', update)

  return { x, y }
}

// composables/useMouseInElement.js - Composes useMouse
import { ref, computed } from 'vue'
import { useMouse } from './useMouse'

export function useMouseInElement(elementRef) {
  const { x, y } = useMouse()

  const elementX = computed(() => {
    if (!elementRef.value) return 0
    const rect = elementRef.value.getBoundingClientRect()
    return x.value - rect.left
  })

  const elementY = computed(() => {
    if (!elementRef.value) return 0
    const rect = elementRef.value.getBoundingClientRect()
    return y.value - rect.top
  })

  const isOutside = computed(() => {
    if (!elementRef.value) return true
    const rect = elementRef.value.getBoundingClientRect()
    return x.value < rect.left || x.value > rect.right ||
           y.value < rect.top || y.value > rect.bottom
  })

  return { x, y, elementX, elementY, isOutside }
}

Pattern: Composable Dependency Chain

// Layer 1: Primitives
export function useEventListener(target, event, callback) { /* ... */ }
export function useInterval(callback, delay) { /* ... */ }
export function useTimeout(callback, delay) { /* ... */ }

// Layer 2: Building on primitives
export function useWindowSize() {
  const width = ref(window.innerWidth)
  const height = ref(window.innerHeight)

  useEventListener(window, 'resize', () => {
    width.value = window.innerWidth
    height.value = window.innerHeight
  })

  return { width, height }
}

export function useOnline() {
  const isOnline = ref(navigator.onLine)

  useEventListener(window, 'online', () => isOnline.value = true)
  useEventListener(window, 'offline', () => isOnline.value = false)

  return { isOnline }
}

// Layer 3: Complex features combining multiple composables
export function useAutoSave(dataRef, saveFunction, options = {}) {
  const { debounce = 1000, onlyWhenOnline = true } = options

  const { isOnline } = useOnline()
  const isSaving = ref(false)
  const lastSaved = ref(null)

  let timeoutId = null

  watch(dataRef, (newData) => {
    if (onlyWhenOnline && !isOnline.value) return

    clearTimeout(timeoutId)
    timeoutId = setTimeout(async () => {
      isSaving.value = true
      try {
        await saveFunction(newData)
        lastSaved.value = new Date()
      } finally {
        isSaving.value = false
      }
    }, debounce)
  }, { deep: true })

  return { isSaving, lastSaved, isOnline }
}

Pattern: Code Organization with Composition

Extract inline composables when a component gets complex:

<script setup>
// BEFORE: All logic mixed together
import { ref, computed, watch, onMounted } from 'vue'

const searchQuery = ref('')
const filters = ref({ category: null, minPrice: 0 })
const products = ref([])
const isLoading = ref(false)
const error = ref(null)
const sortBy = ref('name')
const sortOrder = ref('asc')

// ...50 more lines of mixed concerns
</script>
<script setup>
// AFTER: Separated into focused composables
import { useProductSearch } from './composables/useProductSearch'
import { useProductFilters } from './composables/useProductFilters'
import { useProductSort } from './composables/useProductSort'

const { searchQuery, debouncedQuery } = useProductSearch()
const { filters, activeFilters, clearFilters } = useProductFilters()
const { sortBy, sortOrder, sortedProducts } = useProductSort()

// Each composable is focused, testable, and potentially reusable
</script>

Passing Data Between Composed Composables

// Composables can accept refs from other composables
export function useFilteredProducts(products, filters) {
  return computed(() => {
    let result = toValue(products)

    if (filters.value.category) {
      result = result.filter(p => p.category === filters.value.category)
    }

    if (filters.value.minPrice > 0) {
      result = result.filter(p => p.price >= filters.value.minPrice)
    }

    return result
  })
}

export function useSortedProducts(products, sortConfig) {
  return computed(() => {
    const items = [...toValue(products)]
    const { field, order } = sortConfig.value

    return items.sort((a, b) => {
      const comparison = a[field] > b[field] ? 1 : -1
      return order === 'asc' ? comparison : -comparison
    })
  })
}

// Usage - composables are chained through their outputs
const { products, isLoading } = useFetch('/api/products')
const { filters } = useFilters()
const filteredProducts = useFilteredProducts(products, filters)
const { sortConfig } = useSortConfig()
const sortedProducts = useSortedProducts(filteredProducts, sortConfig)

Advantages Over Mixins

Composables Mixins
Explicit dependencies via imports Implicit dependencies
Clear data flow via parameters Unclear which mixin provides what
No namespace collisions Properties can conflict
Easy to trace and debug Hard to track origins
TypeScript-friendly Poor TypeScript support

Reference