Initial commit

This commit is contained in:
2026-04-17 23:26:01 +00:00
commit 2ea4ca5d52
409 changed files with 63459 additions and 0 deletions

View File

@@ -0,0 +1,290 @@
---
title: Composable Organization Patterns
impact: MEDIUM
impactDescription: Well-structured composables improve maintainability, reusability, and update performance
type: best-practice
tags: [vue3, composables, composition-api, code-organization, api-design, readonly, utilities]
---
# Composable Organization Patterns
**Impact: MEDIUM** - Treat composables as reusable, stateful building blocks and keep their code organized by feature concern. This keeps large components maintainable and prevents hard-to-debug mutation and API design issues.
## Task List
- Compose complex behavior from small, focused composables
- Use options objects for composables with multiple optional parameters
- Return readonly state when updates must flow through explicit actions
- Keep pure utility functions as plain utilities, not composables
- Organize composable and component code by feature concern, and extract composables when components grow
## Compose Composables from Smaller Primitives
**BAD:**
```vue
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
const x = ref(0)
const y = ref(0)
const inside = ref(false)
const el = ref(null)
function onMove(e) {
x.value = e.pageX
y.value = e.pageY
if (!el.value) return
const r = el.value.getBoundingClientRect()
inside.value = x.value >= r.left && x.value <= r.right &&
y.value >= r.top && y.value <= r.bottom
}
onMounted(() => window.addEventListener('mousemove', onMove))
onUnmounted(() => window.removeEventListener('mousemove', onMove))
</script>
```
**GOOD:**
```javascript
// composables/useEventListener.js
import { onMounted, onUnmounted, toValue } from 'vue'
export function useEventListener(target, event, callback) {
onMounted(() => toValue(target).addEventListener(event, callback))
onUnmounted(() => toValue(target).removeEventListener(event, callback))
}
```
```javascript
// composables/useMouse.js
import { ref } from 'vue'
import { useEventListener } from './useEventListener'
export function useMouse() {
const x = ref(0)
const y = ref(0)
useEventListener(window, 'mousemove', (e) => {
x.value = e.pageX
y.value = e.pageY
})
return { x, y }
}
```
```javascript
// composables/useMouseInElement.js
import { computed } from 'vue'
import { useMouse } from './useMouse'
export function useMouseInElement(elementRef) {
const { x, y } = useMouse()
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, isOutside }
}
```
## Use Options Object Pattern for Composable Parameters
**BAD:**
```javascript
export function useFetch(url, method, headers, timeout, retries, immediate) {
// hard to read and easy to misorder
}
useFetch('/api/users', 'GET', null, 5000, 3, true)
```
**GOOD:**
```javascript
export function useFetch(url, options = {}) {
const {
method = 'GET',
headers = {},
timeout = 30000,
retries = 0,
immediate = true
} = options
// implementation
return { method, headers, timeout, retries, immediate }
}
useFetch('/api/users', {
method: 'POST',
timeout: 5000,
retries: 3
})
```
```typescript
interface UseCounterOptions {
initial?: number
min?: number
max?: number
step?: number
}
export function useCounter(options: UseCounterOptions = {}) {
const { initial = 0, min = -Infinity, max = Infinity, step = 1 } = options
// implementation
}
```
## Return Readonly State with Explicit Actions
**BAD:**
```javascript
export function useCart() {
const items = ref([])
const total = computed(() => items.value.reduce((sum, item) => sum + item.price, 0))
return { items, total } // any consumer can mutate directly
}
const { items } = useCart()
items.value.push({ id: 1, price: 10 })
```
**GOOD:**
```javascript
import { ref, computed, readonly } from 'vue'
export function useCart() {
const _items = ref([])
const total = computed(() =>
_items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
function addItem(product, quantity = 1) {
const existing = _items.value.find(item => item.id === product.id)
if (existing) {
existing.quantity += quantity
return
}
_items.value.push({ ...product, quantity })
}
function removeItem(productId) {
_items.value = _items.value.filter(item => item.id !== productId)
}
return {
items: readonly(_items),
total,
addItem,
removeItem
}
}
```
## Keep Utilities as Utilities
**BAD:**
```javascript
export function useFormatters() {
const formatDate = (date) => new Intl.DateTimeFormat('en-US').format(date)
const formatCurrency = (amount) =>
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount)
return { formatDate, formatCurrency }
}
const { formatDate } = useFormatters()
```
**GOOD:**
```javascript
// utils/formatters.js
export function formatDate(date) {
return new Intl.DateTimeFormat('en-US').format(date)
}
export function formatCurrency(amount) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount)
}
```
```javascript
// composables/useInvoiceSummary.js
import { computed } from 'vue'
import { formatCurrency } from '@/utils/formatters'
export function useInvoiceSummary(invoiceRef) {
const totalLabel = computed(() => formatCurrency(invoiceRef.value.total))
return { totalLabel }
}
```
## Organize Composable and Component Code by Feature Concern
**BAD:**
```vue
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
const searchQuery = ref('')
const items = ref([])
const selected = ref(null)
const showModal = ref(false)
const sortBy = ref('name')
const filter = ref('all')
const loading = ref(false)
const filtered = computed(() => items.value.filter(i => i.category === filter.value))
function openModal() { showModal.value = true }
const sorted = computed(() => [...filtered.value].sort(/* ... */))
watch(searchQuery, () => { /* ... */ })
onMounted(() => { /* ... */ })
</script>
```
**GOOD:**
```vue
<script setup>
import { useItems } from '@/composables/useItems'
import { useSearch } from '@/composables/useSearch'
import { useSelectionModal } from '@/composables/useSelectionModal'
// Data
const { items, loading, fetchItems } = useItems()
// Search/filter/sort
const { query, visibleItems } = useSearch(items)
// Selection + modal
const { selectedItem, isModalOpen, selectItem, closeModal } = useSelectionModal()
</script>
```
```javascript
// composables/useItems.js
import { ref, onMounted } from 'vue'
export function useItems() {
const items = ref([])
const loading = ref(false)
async function fetchItems() {
loading.value = true
try {
items.value = await api.getItems()
} finally {
loading.value = false
}
}
onMounted(fetchItems)
return { items, loading, fetchItems }
}
```