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>
5.3 KiB
5.3 KiB
title, impact, impactDescription, type, tags
| title | impact | impactDescription | type | tags | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| Prefer Props and Emit Over Component Refs | MEDIUM | Component refs create tight coupling and break component abstraction | best-practice |
|
Prefer Props and Emit Over Component Refs
Impact: MEDIUM - Using template refs to access child component internals creates tight coupling between parent and child. This makes components harder to maintain, refactor, and reuse. Props and emit provide a cleaner contract-based API that preserves component encapsulation.
Component refs should be reserved for imperative actions (focus, scroll, animations) that can't be expressed declaratively.
Task Checklist
- Use props for passing data down to children
- Use emit for communicating events up to parents
- Only use component refs for imperative DOM operations
- If using refs, expose a minimal, documented API via defineExpose
- Consider if the interaction can be expressed declaratively
Incorrect:
<!-- ParentComponent.vue -->
<script setup>
import { ref, onMounted } from 'vue'
import UserForm from './UserForm.vue'
const formRef = ref(null)
// WRONG: Reaching into child's internals
function submitForm() {
// Tight coupling - parent knows child's internal structure
if (formRef.value.isValid) {
const data = formRef.value.formData
formRef.value.setSubmitting(true)
api.submit(data).then(() => {
formRef.value.setSubmitting(false)
formRef.value.reset()
})
}
}
// WRONG: Parent managing child's state
function prefillForm(userData) {
formRef.value.formData.name = userData.name
formRef.value.formData.email = userData.email
}
</script>
<template>
<UserForm ref="formRef" />
<button @click="submitForm">Submit</button>
</template>
<!-- UserForm.vue - exposing too much -->
<script setup>
import { ref, reactive } from 'vue'
const formData = reactive({ name: '', email: '' })
const isValid = ref(false)
const isSubmitting = ref(false)
function setSubmitting(value) {
isSubmitting.value = value
}
function reset() {
formData.name = ''
formData.email = ''
}
// WRONG: Exposing internal state details
defineExpose({
formData,
isValid,
isSubmitting,
setSubmitting,
reset
})
</script>
Correct:
<!-- ParentComponent.vue -->
<script setup>
import { ref } from 'vue'
import UserForm from './UserForm.vue'
const initialData = ref({ name: '', email: '' })
const isSubmitting = ref(false)
// CORRECT: Child communicates via events
function handleSubmit(formData) {
isSubmitting.value = true
api.submit(formData).finally(() => {
isSubmitting.value = false
})
}
function handleValidChange(isValid) {
console.log('Form validity:', isValid)
}
</script>
<template>
<!-- CORRECT: Props down, events up -->
<UserForm
:initial-data="initialData"
:submitting="isSubmitting"
@submit="handleSubmit"
@valid-change="handleValidChange"
/>
</template>
<!-- UserForm.vue - clean props/emit interface -->
<script setup>
import { reactive, computed, watch } from 'vue'
const props = defineProps({
initialData: { type: Object, default: () => ({}) },
submitting: { type: Boolean, default: false }
})
const emit = defineEmits(['submit', 'valid-change'])
const formData = reactive({ ...props.initialData })
const isValid = computed(() => {
return formData.name.length > 0 && formData.email.includes('@')
})
watch(isValid, (valid) => {
emit('valid-change', valid)
})
function handleSubmit() {
if (isValid.value) {
emit('submit', { ...formData })
}
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<input v-model="formData.name" :disabled="submitting" />
<input v-model="formData.email" :disabled="submitting" />
<button type="submit" :disabled="!isValid || submitting">
{{ submitting ? 'Submitting...' : 'Submit' }}
</button>
</form>
</template>
When Component Refs ARE Appropriate
<!-- CORRECT: Refs for imperative DOM operations -->
<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'
const inputRef = ref(null)
// Imperative focus action - good use of refs
function focusInput() {
inputRef.value?.focus()
}
</script>
<template>
<CustomInput ref="inputRef" v-model="text" />
<button @click="focusInput">Focus Input</button>
</template>
<!-- CustomInput.vue - minimal imperative API -->
<script setup>
import { ref } from 'vue'
const inputEl = ref(null)
// Only expose imperative methods
defineExpose({
focus: () => inputEl.value?.focus(),
blur: () => inputEl.value?.blur(),
select: () => inputEl.value?.select()
})
</script>
<template>
<input ref="inputEl" v-bind="$attrs" />
</template>
Summary
| Use Case | Approach |
|---|---|
| Pass data to child | Props |
| Child notifies parent | Emit events |
| Two-way binding | v-model (props + emit) |
| Focus, scroll, animate | Component ref with minimal expose |
| Access child internal state | Refactor to use props/emit |