Files
agent-skills/skills/vue-best-practices/reference/composable-naming-return-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

3.9 KiB

title, impact, impactDescription, type, tags
title impact impactDescription type tags
Follow Composable Naming Convention and Return Pattern MEDIUM Inconsistent composable patterns lead to confusing APIs and reactivity issues when destructuring best-practice
vue3
composables
composition-api
naming
conventions
refs

Follow Composable Naming Convention and Return Pattern

Impact: MEDIUM - Vue composables should follow established conventions: prefix names with "use" and return plain objects containing refs (not reactive objects). Returning reactive objects causes reactivity loss when destructuring, while inconsistent naming makes code harder to understand.

Task Checklist

  • Name composables with "use" prefix (e.g., useMouse, useFetch, useAuth)
  • Return a plain object containing refs, not a reactive object
  • Allow both destructuring and object-style access
  • Document the returned refs for consumers

Incorrect:

// WRONG: No "use" prefix - unclear it's a composable
export function mousePosition() {
  const x = ref(0)
  const y = ref(0)
  return { x, y }
}

// WRONG: Returning reactive object - destructuring loses reactivity
export function useMouse() {
  const state = reactive({
    x: 0,
    y: 0
  })
  // When consumer destructures: const { x, y } = useMouse()
  // x and y become plain values, not reactive!
  return state
}

// WRONG: Returning single ref directly - inconsistent API
export function useCounter() {
  const count = ref(0)
  return count  // Consumer must use .value everywhere
}

Correct:

// CORRECT: "use" prefix and returns plain object with refs
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 plain object containing refs
  return { x, y }
}

// Consumer can destructure and keep reactivity
const { x, y } = useMouse()
watch(x, (newX) => console.log('x changed:', newX))  // Works!

// Or use as object if preferred
const mouse = useMouse()
console.log(mouse.x.value)

Using reactive() Wrapper for Auto-Unwrapping

If consumers prefer auto-unwrapping (no .value), they can wrap the result:

import { reactive } from 'vue'
import { useMouse } from './composables/useMouse'

// Wrapping in reactive() links the refs
const mouse = reactive(useMouse())

// Now access without .value
console.log(mouse.x)  // Auto-unwrapped, still reactive

// But DON'T destructure from this!
const { x } = reactive(useMouse())  // WRONG: loses reactivity again

Pattern: Returning Both State and Actions

// Composable with state AND methods
export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const doubleCount = computed(() => count.value * 2)

  function increment() {
    count.value++
  }

  function decrement() {
    count.value--
  }

  function reset() {
    count.value = initialValue
  }

  // Return all refs and functions in plain object
  return {
    count,
    doubleCount,
    increment,
    decrement,
    reset
  }
}

// Usage
const { count, doubleCount, increment, reset } = useCounter(10)

Naming Convention Examples

Good Name Bad Name Reason
useFetch fetch Conflicts with native fetch
useAuth authStore "Store" implies Pinia/Vuex
useLocalStorage localStorage Conflicts with native API
useFormValidation validateForm Sounds like a one-shot function
useWindowSize getWindowSize "get" implies synchronous getter

Reference