Vue 3 Composition API Conventions
Cursor.cursorrulesCoding
Vue 3 rules for script-setup components, ref/computed discipline, composable design, and setup-style Pinia stores.
Use when: Use in Vue 3 + TypeScript projects to standardize script setup, composables, and Pinia patterns and to keep Options API code out.
Get this skill
Add the skill file with the CLI, by hand, or over MCP.Save this to your project or home directory so Cursor can load it.
npx goodprompts add vue3-composition-cursorrulesSaves to
./.cursorrulesnpx goodprompts add vue3-composition-cursorrulesSaves to
./.cursorrulesSkill file
.cursorrules3,269 charactersYou are working in a Vue 3 + TypeScript project. Composition API only — never generate Options API (`data()`, `methods`, `mounted()`); if you encounter it, flag it for migration rather than extending it.
## Single-file components
- Always `<script setup lang="ts">`, and block order is `<script setup>` → `<template>` → `<style scoped>`.
- Inside script setup, keep this order: imports, props/emits, composables/stores, local state, computed, watchers, functions, lifecycle hooks.
- Type-based declarations: `defineProps<{ item: Item; dense?: boolean }>()` with `withDefaults` for defaults, and `defineEmits<{ select: [id: string] }>()`. Never the runtime-object syntax.
- Use `defineModel<string>()` for v-model props — do not hand-wire `modelValue` prop + `update:modelValue` emit.
- Component files are PascalCase and multi-word (`UserCard.vue`, never `Card.vue`); reference them in templates as `<UserCard />`.
## Reactivity
- `ref()` for all local state, including objects — `reactive()` is banned in new code because destructuring silently kills its reactivity.
- Derived values are `computed()`, always. A `watch` that only assigns to another ref is a defect: replace with computed.
- `watch` with explicit sources over `watchEffect`; reserve `watchEffect` for fire-and-forget sync with obvious deps. Side-effect watchers that touch timers or listeners must register cleanup via `onWatcherCleanup`/returned stop handles.
- Never destructure props or a store directly. Use `toRefs(props)` / `storeToRefs(store)` when you need standalone refs.
- Templates auto-unwrap refs; in script, never forget `.value` — and never name a ref `value`.
## Composables
- Composables live in `src/composables/useX.ts`, one per file, named `useThing`. They take refs-or-raw via `toValue()` (`MaybeRefOrGetter<T>` params) so callers can pass either.
- A composable returns a plain object of refs and functions (`return { items, isLoading, refresh }`) — never a reactive object, so callers can destructure.
- Composables own their lifecycle: anything they set up (listeners, intervals, observers) is torn down in `onScopeDispose`. A composable the caller must remember to clean up is broken.
- Extract a composable when stateful logic is shared by 2+ components or wraps one external system (`useEventListener`, `useClipboard`). Do not extract single-use logic just to slim a component.
## Pinia
- Setup-style stores only: `export const useCartStore = defineStore("cart", () => { ... })` with refs as state, computeds as getters, functions as actions. No options-object stores.
- Store files: `src/stores/<name>.ts`, store id matches the filename.
- Cross-component shared state goes in a store; parent-child state stays in props/emits. Do not create a store to avoid passing two props.
- Async actions set their own `isLoading`/`error` refs inside the store; components never duplicate that bookkeeping.
- Mutate state only inside store actions. Components read via `storeToRefs` and call actions — no `store.count++` from a component.
## Templates
- `v-for` always has `:key` bound to a stable id, never the index. Never put `v-if` and `v-for` on the same element — filter in a computed.
- Multi-value class bindings use the object/array syntax, not string concatenation.