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>
209 lines
5.2 KiB
Markdown
209 lines
5.2 KiB
Markdown
---
|
|
title: Use Composables Instead of Mixins for Logic Reuse
|
|
impact: HIGH
|
|
impactDescription: Mixins cause naming conflicts, unclear data origins, and inflexible logic - composables solve all these problems
|
|
type: best-practice
|
|
tags: [vue3, composition-api, composables, mixins, refactoring, code-reuse]
|
|
---
|
|
|
|
# Use Composables Instead of Mixins for Logic Reuse
|
|
|
|
**Impact: HIGH** - Mixins, the primary logic reuse mechanism in Options API, have fundamental flaws that make code hard to maintain. Composables (Composition API functions) solve all mixin drawbacks: unclear property origins, naming conflicts, and inability to parameterize.
|
|
|
|
The ability to create clean, reusable logic through composables is the primary advantage of the Composition API.
|
|
|
|
## Task Checklist
|
|
|
|
- [ ] Migrate existing mixins to composables when refactoring
|
|
- [ ] Never create new mixins - use composables instead
|
|
- [ ] Use explicit imports to make data origins clear
|
|
- [ ] Parameterize composables to make them flexible
|
|
- [ ] Prefix composables with "use" (useAuth, useFetch, useForm)
|
|
|
|
**Problems with Mixins:**
|
|
```javascript
|
|
// userMixin.js
|
|
export const userMixin = {
|
|
data() {
|
|
return {
|
|
user: null,
|
|
loading: false // Conflict waiting to happen!
|
|
}
|
|
},
|
|
methods: {
|
|
fetchUser() { /* ... */ }
|
|
}
|
|
}
|
|
|
|
// authMixin.js
|
|
export const authMixin = {
|
|
data() {
|
|
return {
|
|
token: null,
|
|
loading: false // NAME CONFLICT with userMixin!
|
|
}
|
|
},
|
|
methods: {
|
|
login() { /* ... */ }
|
|
}
|
|
}
|
|
|
|
// Component using mixins - PROBLEMATIC
|
|
export default {
|
|
mixins: [userMixin, authMixin],
|
|
|
|
mounted() {
|
|
// PROBLEM 1: Where does 'user' come from? Have to check mixins
|
|
console.log(this.user)
|
|
|
|
// PROBLEM 2: Which 'loading'? Last mixin wins, silently!
|
|
console.log(this.loading) // Is this user loading or auth loading?
|
|
|
|
// PROBLEM 3: Can't customize behavior per-component
|
|
this.fetchUser() // Always fetches the same way
|
|
}
|
|
}
|
|
```
|
|
|
|
**Composables Solution:**
|
|
```javascript
|
|
// composables/useUser.js
|
|
import { ref } from 'vue'
|
|
|
|
export function useUser(userId) { // Can accept parameters!
|
|
const user = ref(null)
|
|
const loading = ref(false)
|
|
const error = ref(null)
|
|
|
|
async function fetchUser() {
|
|
loading.value = true
|
|
try {
|
|
user.value = await api.getUser(userId)
|
|
} catch (e) {
|
|
error.value = e
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
return { user, loading, error, fetchUser }
|
|
}
|
|
|
|
// composables/useAuth.js
|
|
import { ref } from 'vue'
|
|
|
|
export function useAuth() {
|
|
const token = ref(null)
|
|
const loading = ref(false) // No conflict - it's scoped!
|
|
|
|
async function login(credentials) { /* ... */ }
|
|
function logout() { /* ... */ }
|
|
|
|
return { token, loading, login, logout }
|
|
}
|
|
|
|
// Component using composables - CLEAR AND FLEXIBLE
|
|
<script setup>
|
|
import { useUser } from '@/composables/useUser'
|
|
import { useAuth } from '@/composables/useAuth'
|
|
|
|
// SOLUTION 1: Clear where everything comes from
|
|
const { user, loading: userLoading, fetchUser } = useUser(123)
|
|
const { token, loading: authLoading, login } = useAuth()
|
|
|
|
// SOLUTION 2: Rename to avoid any conflicts
|
|
// userLoading vs authLoading - explicit!
|
|
|
|
// SOLUTION 3: Parameterize behavior
|
|
const adminUser = useUser(adminId)
|
|
const currentUser = useUser(currentUserId)
|
|
// Each has its own state!
|
|
|
|
onMounted(() => {
|
|
fetchUser() // Explicitly from useUser
|
|
})
|
|
</script>
|
|
```
|
|
|
|
## Migrating from Mixins
|
|
|
|
```javascript
|
|
// BEFORE: Mixin with options
|
|
export const formMixin = {
|
|
data() {
|
|
return { errors: {}, submitting: false }
|
|
},
|
|
methods: {
|
|
validate() { /* ... */ },
|
|
submit() { /* ... */ }
|
|
}
|
|
}
|
|
|
|
// AFTER: Composable with flexibility
|
|
export function useForm(initialValues, validationSchema) {
|
|
const values = ref({ ...initialValues })
|
|
const errors = ref({})
|
|
const submitting = ref(false)
|
|
const touched = ref({})
|
|
|
|
function validate() {
|
|
errors.value = validationSchema.validate(values.value)
|
|
return Object.keys(errors.value).length === 0
|
|
}
|
|
|
|
async function submit(onSubmit) {
|
|
if (!validate()) return
|
|
|
|
submitting.value = true
|
|
try {
|
|
await onSubmit(values.value)
|
|
} finally {
|
|
submitting.value = false
|
|
}
|
|
}
|
|
|
|
function reset() {
|
|
values.value = { ...initialValues }
|
|
errors.value = {}
|
|
touched.value = {}
|
|
}
|
|
|
|
return {
|
|
values,
|
|
errors,
|
|
submitting,
|
|
touched,
|
|
validate,
|
|
submit,
|
|
reset
|
|
}
|
|
}
|
|
|
|
// Usage - now parameterizable and explicit
|
|
const loginForm = useForm(
|
|
{ email: '', password: '' },
|
|
loginValidationSchema
|
|
)
|
|
|
|
const registerForm = useForm(
|
|
{ email: '', password: '', name: '' },
|
|
registerValidationSchema
|
|
)
|
|
```
|
|
|
|
## Composition Over Mixins Benefits
|
|
|
|
| Aspect | Mixins | Composables |
|
|
|--------|--------|-------------|
|
|
| Property origin | Unclear | Explicit import |
|
|
| Naming conflicts | Silent overwrites | Explicit rename |
|
|
| Parameters | Not possible | Fully supported |
|
|
| Type inference | Poor | Excellent |
|
|
| Reuse instances | One per component | Multiple allowed |
|
|
| Tree-shaking | Not possible | Fully supported |
|
|
|
|
## Reference
|
|
- [Composition API FAQ - Better Logic Reuse](https://vuejs.org/guide/extras/composition-api-faq.html#better-logic-reuse)
|
|
- [Composables](https://vuejs.org/guide/reusability/composables.html)
|
|
- [VueUse - Collection of Composables](https://vueuse.org/)
|