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>
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
---
|
||||
title: Compose Composables for Complex Logic
|
||||
impact: MEDIUM
|
||||
impactDescription: Building composables from other composables creates reusable, testable building blocks
|
||||
type: best-practice
|
||||
tags: [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**
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```javascript
|
||||
// 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:
|
||||
|
||||
```vue
|
||||
<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>
|
||||
```
|
||||
|
||||
```vue
|
||||
<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
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
- [Vue.js Composables](https://vuejs.org/guide/reusability/composables.html)
|
||||
- [Vue.js Composables vs Mixins](https://vuejs.org/guide/reusability/composables.html#comparisons-with-other-techniques)
|
||||
Reference in New Issue
Block a user