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>
214 lines
5.9 KiB
Markdown
214 lines
5.9 KiB
Markdown
---
|
|
title: Organize Composition API Code by Logical Concern, Not Option Type
|
|
impact: MEDIUM
|
|
impactDescription: Poor code organization in Composition API leads to spaghetti code worse than Options API
|
|
type: best-practice
|
|
tags: [vue3, composition-api, code-organization, refactoring, maintainability]
|
|
---
|
|
|
|
# Organize Composition API Code by Logical Concern, Not Option Type
|
|
|
|
**Impact: MEDIUM** - The Composition API removes the "guard rails" of Options API that force code into data/methods/computed buckets. Without intentional organization, Composition API code can become more disorganized than Options API. Group related code together by feature or logical concern.
|
|
|
|
The key insight is that Composition API gives you flexibility - which requires discipline. Apply the same code organization principles you would use for any well-structured JavaScript code.
|
|
|
|
## Task Checklist
|
|
|
|
- [ ] Group related state, computed, and methods together by feature
|
|
- [ ] Extract related logic into composables when it grows
|
|
- [ ] Don't scatter related code throughout the script section
|
|
- [ ] Use comments or regions to delineate logical sections in larger components
|
|
- [ ] Consider splitting large components into smaller ones or composables
|
|
|
|
**Disorganized (Bad):**
|
|
```vue
|
|
<script setup>
|
|
// Scattered code - hard to understand what relates to what
|
|
import { ref, computed, onMounted, watch } from 'vue'
|
|
|
|
const searchQuery = ref('')
|
|
const items = ref([])
|
|
const selectedItem = ref(null)
|
|
const isModalOpen = ref(false)
|
|
const sortOrder = ref('asc')
|
|
const filterCategory = ref('all')
|
|
const isLoading = ref(false)
|
|
const error = ref(null)
|
|
|
|
const filteredItems = computed(() => {
|
|
return items.value.filter(i => i.category === filterCategory.value)
|
|
})
|
|
|
|
function openModal() { isModalOpen.value = true }
|
|
|
|
const sortedItems = computed(() => {
|
|
return [...filteredItems.value].sort(/* ... */)
|
|
})
|
|
|
|
function closeModal() { isModalOpen.value = false }
|
|
|
|
watch(searchQuery, async (query) => { /* fetch */ })
|
|
|
|
function selectItem(item) { selectedItem.value = item }
|
|
|
|
const searchResults = computed(() => {
|
|
return items.value.filter(i => i.name.includes(searchQuery.value))
|
|
})
|
|
|
|
onMounted(() => { fetchItems() })
|
|
|
|
async function fetchItems() { /* ... */ }
|
|
</script>
|
|
```
|
|
|
|
**Organized by Concern (Good):**
|
|
```vue
|
|
<script setup>
|
|
import { ref, computed, onMounted, watch } from 'vue'
|
|
|
|
// ============================================
|
|
// DATA FETCHING & STATE
|
|
// ============================================
|
|
const items = ref([])
|
|
const isLoading = ref(false)
|
|
const error = ref(null)
|
|
|
|
async function fetchItems() {
|
|
isLoading.value = true
|
|
try {
|
|
items.value = await api.getItems()
|
|
} catch (e) {
|
|
error.value = e
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => fetchItems())
|
|
|
|
// ============================================
|
|
// SEARCH
|
|
// ============================================
|
|
const searchQuery = ref('')
|
|
|
|
const searchResults = computed(() =>
|
|
items.value.filter(i =>
|
|
i.name.toLowerCase().includes(searchQuery.value.toLowerCase())
|
|
)
|
|
)
|
|
|
|
watch(searchQuery, async (query) => {
|
|
if (query.length > 2) {
|
|
await fetchItems({ search: query })
|
|
}
|
|
})
|
|
|
|
// ============================================
|
|
// FILTERING & SORTING
|
|
// ============================================
|
|
const filterCategory = ref('all')
|
|
const sortOrder = ref('asc')
|
|
|
|
const filteredItems = computed(() =>
|
|
searchResults.value.filter(i =>
|
|
filterCategory.value === 'all' || i.category === filterCategory.value
|
|
)
|
|
)
|
|
|
|
const sortedItems = computed(() =>
|
|
[...filteredItems.value].sort((a, b) =>
|
|
sortOrder.value === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
|
|
)
|
|
)
|
|
|
|
// ============================================
|
|
// SELECTION & MODAL
|
|
// ============================================
|
|
const selectedItem = ref(null)
|
|
const isModalOpen = ref(false)
|
|
|
|
function selectItem(item) {
|
|
selectedItem.value = item
|
|
openModal()
|
|
}
|
|
|
|
function openModal() { isModalOpen.value = true }
|
|
function closeModal() {
|
|
isModalOpen.value = false
|
|
selectedItem.value = null
|
|
}
|
|
</script>
|
|
```
|
|
|
|
**Best: Extract to Composables:**
|
|
```vue
|
|
<script setup>
|
|
import { useItems } from '@/composables/useItems'
|
|
import { useSearch } from '@/composables/useSearch'
|
|
import { useModal } from '@/composables/useModal'
|
|
|
|
// Each composable encapsulates a logical concern
|
|
const { items, isLoading, error, fetchItems } = useItems()
|
|
const { searchQuery, searchResults } = useSearch(items)
|
|
const {
|
|
selectedItem,
|
|
isOpen: isModalOpen,
|
|
open: openModal,
|
|
close: closeModal
|
|
} = useModal()
|
|
|
|
function selectItem(item) {
|
|
selectedItem.value = item
|
|
openModal()
|
|
}
|
|
</script>
|
|
|
|
// composables/useItems.js
|
|
export function useItems() {
|
|
const items = ref([])
|
|
const isLoading = ref(false)
|
|
const error = ref(null)
|
|
|
|
async function fetchItems(params = {}) {
|
|
isLoading.value = true
|
|
try {
|
|
items.value = await api.getItems(params)
|
|
} catch (e) {
|
|
error.value = e
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => fetchItems())
|
|
|
|
return { items, isLoading, error, fetchItems }
|
|
}
|
|
```
|
|
|
|
## Signs Your Component Needs Refactoring
|
|
|
|
1. **Scrolling between related code** - If you're jumping around to understand one feature
|
|
2. **300+ lines in script setup** - Consider extracting composables
|
|
3. **Multiple unrelated features** - Each should be its own composable
|
|
4. **Similar patterns repeated** - Extract to shared composable
|
|
|
|
## When to Extract to Composables
|
|
|
|
```javascript
|
|
// Extract when:
|
|
// - Logic is reused across components
|
|
// - A feature is self-contained (search, pagination, form handling)
|
|
// - Component is getting too large (>200 lines)
|
|
// - You want to test logic in isolation
|
|
|
|
// Keep inline when:
|
|
// - Logic is simple and component-specific
|
|
// - Extracting would add more complexity than it removes
|
|
// - The component is already small and focused
|
|
```
|
|
|
|
## Reference
|
|
- [Composition API FAQ - More Flexible Code Organization](https://vuejs.org/guide/extras/composition-api-faq.html#more-flexible-code-organization)
|
|
- [Composables](https://vuejs.org/guide/reusability/composables.html)
|