vue3-composition
Skill2 starsChanged 34 days agoMITView on GitHub
Use when writing or refactoring Vue 3 components and composables with <script setup>. Enforces reactivity choices (ref vs reactive vs shallowRef), watcher discipline, typed props/emits, and composable boundaries that keep components small and testable.
2 other projects use a version of this.
Adopted by
Most recently taken up by
- MsNan/vue-uniapp-skillstheir own version, first seen Oct 17, 2026
- LeonxLJX/web-engineer-skillstheir own version, first seen Sep 3, 2026
Other versions2 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 2
- LeonxLJX/web-engineer-skills★ 0same name
- MsNan/vue-uniapp-skills★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository13 other files it gives its agents.
- api-layer-axiosSkill
- element-plus-adminSkill
- nuxt3-appSkill
- pinia-stateSkill
- uni-app-cross-platformSkill
- vue2-to-vue3-migrationSkill
- vue-code-reviewSkill
- vue-forms-validationSkill
- vue-i18n-setupSkill
- vue-performanceSkill
- vue-router-authSkill
- vue-testing-vitestSkill
- wechat-miniprogramSkill
The file
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 0 are distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 31
computedis pure: No side effects, no await, no writes to other state inside a computed getter. - Very specific: line 45
One component does one thing: When a .vue file passes ~300 lines or holds three unrelated refs groups, extract a composable or a child …
- Very specific: line 51
Read the existing conventions first: Check two neighbouring components for import style, auto-import usage (unplugin-auto-import means no …
- Very specific: line 63
Run
vue-tsc --noEmit(or the project's type-check script): before claiming the change compiles. - Very specific: line 143
Use only for component trees that are truly coupled (a Table and its Columns).
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Vue 3 Composition API conventions
This skill applies to any .vue file or use*.ts composable in a Vue 3.3+
project. It assumes <script setup lang="ts">. It is not a Vue tutorial: it
encodes the choices that separate a component that survives two years of
feature requests from one that gets rewritten. Skip it for Options API code
that is not being migrated.
Non-negotiables
<script setup lang="ts">for every new component. Options API only when touching an existing Options API file and the change is under 20 lines.ref()by default,reactive()only for a group of fields that always travel together (a form model).reactiveloses reactivity on destructuring and on reassignment, which is the #1 "my state stopped updating" bug.shallowRef()for large or external data: API lists over ~1k rows, chart instances, editor instances, map objects. Deep proxies on those cost tens of milliseconds per assignment and break third-party libraries that compare identity.- Never mutate props. Emit an event or use
defineModel(). If you need a local editable copy, create it explicitly withref(structuredClone(toRaw(props.x)))and say why in a comment. - Type props with
defineProps<{...}>()and emits withdefineEmits<{...}>(). Runtime-object declarations are allowed only when a prop needs a non-trivialvalidator. computedis pure. No side effects, noawait, no writes to other state inside a computed getter. If you need a writable derived value, usecomputed({ get, set })and keep the setter one line.- Every
watchhas an explicit reason written next to it. If the reason is "sync A into B", replace it with acomputed. If the reason is "fetch when the id changes", preferwatch(() => props.id, load, { immediate: true })overonMountedpluswatch. - Composables return refs, not reactive objects, so callers can
destructure. Return
{ data, loading, error, refresh }, not areactivebag. - Clean up what you create. Timers, event listeners, observers and
subscriptions created in
setupare removed inonUnmountedor viaonScopeDisposeinside composables. Memory leaks in admin apps with<keep-alive>tabs come from here. - One component does one thing. When a
.vuefile passes ~300 lines or holds three unrelatedrefs groups, extract a composable or a child component. Do not extract for its own sake under 150 lines.
Workflow
- Read the existing conventions first. Check two neighbouring components
for import style, auto-import usage (
unplugin-auto-importmeans noimport { ref } from 'vue'), naming (useXxx,XxxView.vue,XxxDialog.vue) and whether the project usesdefineOptions({ name }). - Decide the state shape before writing template. List the pieces of
state, mark each as local / prop / store / server, and pick
ref,reactive,shallowRefdeliberately. - Write the composable when logic is reused or exceeds ~40 lines, even if it is used once today. Keep it in the same folder as the component unless a second consumer appears.
- Write the template last, binding only to refs/computeds. No business
logic in template expressions beyond
&&,??and a formatter call. - Run
vue-tsc --noEmit(or the project's type-check script) before claiming the change compiles. A template type error does not show up invite dev.
Patterns
Props with defaults
// wrong: loses types, defaults buried in runtime object
const props = defineProps({ size: { type: String, default: 'md' } })
// right
const props = withDefaults(defineProps<{ size?: 'sm' | 'md' | 'lg'; items: Item[] }>(), {
size: 'md',
})Two-way binding
// wrong: manual prop + emit pair for a simple value
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ 'update:modelValue': [v: string] }>()
// right (Vue 3.4+)
const value = defineModel<string>({ required: true })Async data in a composable
export function useList<T>(fetcher: (q: Query) => Promise<Page<T>>) {
const rows = shallowRef<T[]>([]) // large list, shallow on purpose
const total = ref(0)
const loading = ref(false)
const error = ref<unknown>(null)
let seq = 0 // drop out-of-order responses
async function load(q: Query) {
const my = ++seq
loading.value = true
error.value = null
try {
const page = await fetcher(q)
if (my !== seq) return // a newer request already landed
rows.value = page.items
total.value = page.total
} catch (e) {
if (my === seq) error.value = e
} finally {
if (my === seq) loading.value = false
}
}
return { rows, total, loading, error, load }
}The sequence counter is mandatory in any list composable: fast typing in a search box otherwise shows stale results.
Watch vs computed
// wrong: watcher used to derive state
watch(items, (v) => { count.value = v.length })
// right
const count = computed(() => items.value.length)Template refs on child components
const formRef = ref<InstanceType<typeof ElForm>>() // Element Plus exposes FormInstance too
// wrong: formRef.value.validate() right after toggling v-if
// right: await nextTick() first, or keep the form mounted and use v-showProvide / inject
Use only for component trees that are truly coupled (a Table and its
Columns). For app-wide values use a Pinia store. Always type the key:
export const TableKey: InjectionKey<TableContext> = Symbol('table')Pitfalls
| Symptom | Cause | Fix |
| Destructured state is frozen | const { a } = reactive(...) | Use toRefs(state) or switch to refs |
| List re-renders every keystroke, UI lags | Deep reactive on 5k rows | shallowRef, replace the array instead of pushing |
| Chart/editor library throws on proxied object | Instance wrapped in ref | shallowRef or markRaw(instance) |
| Watcher fires on mount but data undefined | immediate: true with async source | Guard if (!id) return at top of callback |
watch on props.x never fires | Watching a plain value, not a getter | watch(() => props.x, ...) |
| Deep watcher costs 50ms per change | { deep: true } on a large object | Watch the specific field, or use watchEffect with precise reads |
| Component keeps running after tab closed | Timer or listener not cleared | onUnmounted / onScopeDispose |
await before defineProps breaks the component | Top-level await in <script setup> without <Suspense> | Move the await into onMounted or a composable's load() |
| Parent calls child method that is undefined | <script setup> is closed by default | defineExpose({ validate, reset }) |
| Type error only in CI | Template errors invisible to vite dev | Run vue-tsc --noEmit locally |
Before you finish
- No
reactiveis destructured and no prop is mutated. - Every
watchhas a comment saying why it is not acomputed. - Lists above ~1k rows and third-party instances use
shallowRef/markRaw. - Timers, listeners, observers created in setup are cleaned up.
- Async loaders drop out-of-order responses.
- Props and emits are typed;
defineExposecovers what the parent calls. vue-tsc --noEmitpasses, and the file is under ~300 lines or has a reason.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

