|
| 1 | +--- |
| 2 | +description: 'Comprehensive Vue 3 development standards and best practices: Composition API, `<script setup>`, the full reactivity system, compiler macros (defineModel/defineSlots/defineOptions), built-in components (Teleport/Suspense/Transition/KeepAlive), provide/inject, composables, Pinia, Vue Router, TypeScript, testing, performance, SSR, and security.' |
| 3 | +applyTo: '**/*.vue, **/*.ts, **/*.js, **/*.css, **/*.scss' |
| 4 | +--- |
| 5 | + |
| 6 | +# Vue 3 Development Instructions |
| 7 | + |
| 8 | +Authoritative guidance for building production-grade Vue 3 applications. Default to the **Composition API** with `<script setup lang="ts">`, the modern reactivity system, and the official ecosystem (Pinia, Vue Router, Vite, Vitest). Prefer the idioms below over legacy Options API and Vue 2 patterns. |
| 9 | + |
| 10 | +## Project Context |
| 11 | +- Vue 3.4+ (use 3.5+ features such as `useTemplateRef`, `useId`, and reactive props destructuring where the project's version allows). |
| 12 | +- `<script setup lang="ts">` single-file components (SFCs) as the default authoring style. |
| 13 | +- TypeScript everywhere: components, composables, stores, and router. |
| 14 | +- Pinia for state management; Vue Router for routing; Vite for build/dev. |
| 15 | +- Vitest + Vue Test Utils (or Testing Library for Vue) for tests. |
| 16 | + |
| 17 | +## Authoring Style & Component Design |
| 18 | +- Use `<script setup>` — it is more concise, faster, and has better type inference than `setup()` or the Options API. |
| 19 | +- One responsibility per component; split large components into smaller focused ones plus composables. |
| 20 | +- Order an SFC as `<script setup>`, then `<template>`, then `<style scoped>`. |
| 21 | +- Name components in PascalCase; use multi-word names (e.g. `UserCard`, not `Card`) to avoid clashing with native elements. |
| 22 | +- Co-locate component-specific types, and lift shared types into a `types/` module. |
| 23 | + |
| 24 | +## Compiler Macros (no imports needed) |
| 25 | +- `defineProps<T>()` — declare typed props from a TypeScript interface/type for full inference. |
| 26 | +- `withDefaults(defineProps<T>(), { ... })` — provide prop defaults (or use reactive props destructuring with defaults in 3.5+). |
| 27 | +- `defineEmits<{ change: [id: number]; update: [value: string] }>()` — declare typed events. |
| 28 | +- `defineModel<T>()` (3.4+) — the canonical way to implement `v-model` on a component; supports multiple models, arguments, and modifiers. |
| 29 | +- `defineExpose({ ... })` — explicitly expose a public imperative API; expose nothing by default. |
| 30 | +- `defineSlots<{ default(props: { item: T }): any }>()` — type named/scoped slots. |
| 31 | +- `defineOptions({ name, inheritAttrs })` — set component options inside `<script setup>`. |
| 32 | +- Never mutate props directly — emit an event, use `defineModel`, or derive local state with `computed`/`ref`. |
| 33 | + |
| 34 | +## Reactivity System |
| 35 | +### Core primitives |
| 36 | +- `ref()` for primitives and single replaceable references; access via `.value` in script (auto-unwrapped in templates). |
| 37 | +- `reactive()` for deep-reactive objects/collections; never destructure it directly (breaks reactivity) — use `toRefs()`/`toRef()`. |
| 38 | +- `computed()` for derived values; keep getters pure and side-effect free. Use writable computed (`get`/`set`) for two-way derived state. |
| 39 | +- Prefer `computed` over `watch` whenever you are *deriving* a value rather than performing a side effect. |
| 40 | + |
| 41 | +### Watchers |
| 42 | +- `watch(source, cb, options)` for explicit dependencies; `watchEffect(cb)` for auto-tracked dependencies. |
| 43 | +- Use watch options deliberately: `{ immediate: true }`, `{ deep: true }`, `{ once: true }` (3.4+), and `flush: 'post'` when you need the DOM updated first. |
| 44 | +- Register cleanup with the `onCleanup`/`onWatcherCleanup` callback to cancel stale async work (debounce, fetch, listeners). |
| 45 | +- Stop manual watchers via their returned handle when they outlive their natural scope. |
| 46 | + |
| 47 | +### Advanced reactivity (use intentionally) |
| 48 | +- `shallowRef` / `shallowReactive` for large or externally-managed data to skip deep tracking. |
| 49 | +- `readonly()` to hand out immutable views of shared state. |
| 50 | +- `toRef` / `toRefs` to keep reactivity when destructuring; `toRaw`/`markRaw` to opt out for non-reactive objects (e.g. class instances, 3rd-party clients). |
| 51 | +- `effectScope()` to group and dispose related effects together (useful in composables/libraries). |
| 52 | +- `customRef` for debounced/throttled or storage-backed refs. |
| 53 | + |
| 54 | +## Composables (reusable logic) |
| 55 | +- Extract stateful, reusable logic into `useXxx()` functions under `composables/`. |
| 56 | +- Accept refs/getters as inputs and return refs/computed; use `toValue()`/`MaybeRefOrGetter` to normalize ref-or-plain inputs. |
| 57 | +- Set up and tear down inside the composable (`onMounted`/`onUnmounted` or `tryOnScopeDispose`) so callers don't leak. |
| 58 | +- Keep composables synchronous in their setup phase; expose async actions as returned functions. |
| 59 | +- Reach for VueUse for common needs instead of re-implementing (e.g. `useStorage`, `useEventListener`, `useDebounceFn`). |
| 60 | + |
| 61 | +## Lifecycle & Effects |
| 62 | +- Use `onMounted`, `onBeforeMount`, `onUpdated`, `onBeforeUnmount`, `onUnmounted`, `onActivated`/`onDeactivated` (with `<KeepAlive>`), and `onErrorCaptured`. |
| 63 | +- Always clean up timers, listeners, observers, and subscriptions in `onUnmounted`. |
| 64 | +- Guard browser-only APIs (`window`, `document`) for SSR; run them in `onMounted`. |
| 65 | + |
| 66 | +## Template Best Practices |
| 67 | +- Always set a stable, unique `:key` on `v-for`; never use the array index when items can reorder or mutate. |
| 68 | +- Never put `v-if` and `v-for` on the same element — filter via a `computed` instead. |
| 69 | +- `v-show` for frequently toggled elements; `v-if` for conditional mounting. |
| 70 | +- Use `v-memo` to skip re-rendering of expensive static subtrees, and `v-once` for content that renders a single time. |
| 71 | +- Use the `:` (v-bind) and `@` (v-on) shorthands consistently; group nodes with `<template>` to avoid wrapper elements. |
| 72 | +- Avoid heavy expressions in templates — move them to `computed` or methods. |
| 73 | + |
| 74 | +## Slots |
| 75 | +- Use named slots for layout extension and scoped slots (`<slot :item="item" />` + `#default="{ item }"`) to expose data to the parent. |
| 76 | +- Provide sensible fallback slot content. |
| 77 | +- Use the `v-slot` (`#`) shorthand and dynamic slot names where appropriate. |
| 78 | + |
| 79 | +## Built-in Components |
| 80 | +- `<Teleport to="body">` for modals, toasts, and tooltips that must escape overflow/stacking contexts. |
| 81 | +- `<Suspense>` with `#default`/`#fallback` for async setup and lazy components; pair with error handling. |
| 82 | +- `<Transition>` / `<TransitionGroup>` for enter/leave and list animations (set `:key` on grouped items). |
| 83 | +- `<KeepAlive>` (with `include`/`exclude`/`max`) to cache component state across toggles; handle `onActivated`/`onDeactivated`. |
| 84 | +- `<component :is="...">` for dynamic components; `defineAsyncComponent(() => import('...'))` for code-split/lazy loading with loading/error components. |
| 85 | + |
| 86 | +## Provide / Inject (dependency injection) |
| 87 | +- Type injections with an `InjectionKey<T>` (`Symbol`) for safety: `provide(key, value)` / `inject(key)`. |
| 88 | +- Provide a default or assert presence to avoid `undefined`. |
| 89 | +- Prefer `readonly()` when providing state that children should not mutate; expose explicit updater functions instead. |
| 90 | +- Use injection for cross-cutting concerns; use Pinia for app-wide shared state. |
| 91 | + |
| 92 | +## Custom Directives & Plugins |
| 93 | +- Author directives as objects with `mounted`/`updated` (etc.) hooks; keep them DOM-focused (e.g. `v-focus`, `v-click-outside`). |
| 94 | +- Encapsulate global setup (router, pinia, i18n, UI libs) as plugins via `app.use(...)`; register global config in `main.ts`. |
| 95 | + |
| 96 | +## State Management with Pinia |
| 97 | +- Use Pinia for shared/cross-component state; keep component-only state local with `ref`/`reactive`. |
| 98 | +- Prefer **setup stores**: `defineStore('user', () => { const user = ref(...); const isLoggedIn = computed(...); function login(){}; return { user, isLoggedIn, login } })`. |
| 99 | +- One store per domain; keep actions for async/side effects and getters pure & synchronous. |
| 100 | +- Destructure with `storeToRefs()` to preserve reactivity; use `$patch` for batched mutations, `$reset` to restore state, and `$subscribe`/`$onAction` for cross-cutting concerns. |
| 101 | +- Handle SSR hydration correctly and keep stores serializable. |
| 102 | + |
| 103 | +## Routing with Vue Router |
| 104 | +- Define routes with lazy `component: () => import('...')` for automatic code splitting. |
| 105 | +- Use navigation guards (`beforeEach`, `beforeEnter`, `beforeRouteLeave`) for auth and unsaved-changes checks; always resolve/`next()` exactly once. |
| 106 | +- Use `route.meta` (typed) for per-route config like `requiresAuth`. |
| 107 | +- Read params/query via `useRoute()` and navigate via `useRouter()`; treat `route.params` as reactive (watch it, don't cache). |
| 108 | +- Configure `scrollBehavior` for predictable scroll restoration; enable typed routes where available. |
| 109 | + |
| 110 | +## TypeScript Integration |
| 111 | +- Type props/emits/slots through generic compiler macros, not runtime object syntax. |
| 112 | +- Type refs explicitly when inference is too narrow: `ref<User | null>(null)`. |
| 113 | +- Type template refs with `useTemplateRef<HTMLInputElement>('input')` (3.5+) or `ref<HTMLInputElement | null>(null)`. |
| 114 | +- Build generic components with `<script setup lang="ts" generic="T">`. |
| 115 | +- Type provide/inject with `InjectionKey<T>`; type Pinia stores via their inferred return types. |
| 116 | + |
| 117 | +## Styling |
| 118 | +- Default to `<style scoped>`; use `:deep()`, `:slotted()`, and `:global()` selectors deliberately. |
| 119 | +- Use `v-bind()` in `<style>` to drive CSS from reactive state; prefer CSS custom properties for theming. |
| 120 | +- Consider CSS Modules (`<style module>`) for class-name isolation in larger teams. |
| 121 | + |
| 122 | +## Forms & Validation |
| 123 | +- Bind inputs with `v-model` (and `defineModel` for custom inputs); use modifiers `.lazy`, `.number`, `.trim`. |
| 124 | +- Validate with a schema library (Zod/Yup) plus a form library (VeeValidate or FormKit) for non-trivial forms. |
| 125 | +- Client validation is for UX only — always validate and sanitize on the server. |
| 126 | + |
| 127 | +## Error Handling |
| 128 | +- Use `onErrorCaptured` for component-tree boundaries and `app.config.errorHandler` for a global hook. |
| 129 | +- Wrap async/lazy boundaries with `<Suspense>` + an error fallback. |
| 130 | +- Surface user-friendly errors; log diagnostics to your monitoring pipeline. |
| 131 | + |
| 132 | +## Performance |
| 133 | +- Code-split routes and heavy components (`defineAsyncComponent`, dynamic `import()`). |
| 134 | +- Use `computed` for caching, `v-memo`/`v-once` for static subtrees, and `shallowRef`/`shallowReactive` for big datasets. |
| 135 | +- Virtualize long lists (e.g. `vue-virtual-scroller`); paginate or window large data. |
| 136 | +- Avoid unnecessary deep reactivity and avoid creating new object/array literals inline in templates. |
| 137 | +- In 3.5+, consider lazy hydration strategies for SSR to reduce time-to-interactive. |
| 138 | + |
| 139 | +## SSR / Meta-frameworks |
| 140 | +- Prefer Nuxt for SSR/SSG/hybrid rendering unless you have a reason to hand-roll SSR. |
| 141 | +- Keep code isomorphic: guard browser APIs, avoid module-level shared mutable state across requests, and ensure stores are request-scoped. |
| 142 | +- Match server and client output to prevent hydration mismatches. |
| 143 | + |
| 144 | +## Accessibility |
| 145 | +- Use semantic HTML first; add ARIA only to fill genuine gaps. |
| 146 | +- Ensure full keyboard operability and visible focus states. |
| 147 | +- Manage focus on route changes and when opening/closing dialogs (trap focus in modals). |
| 148 | +- Give icon-only controls accessible names (`aria-label`); associate labels with inputs. |
| 149 | + |
| 150 | +## Security |
| 151 | +- Never render untrusted input with `v-html`; sanitize (e.g. DOMPurify) if it is unavoidable. |
| 152 | +- Avoid dynamic `:is`/`:href`/`:src` from untrusted sources; validate URLs (block `javascript:` schemes). |
| 153 | +- Keep secrets server-side; only expose `VITE_`-prefixed env vars intentionally meant to be public. |
| 154 | +- Apply a Content Security Policy and standard CSRF/XSS protections at the app layer. |
| 155 | + |
| 156 | +## Testing |
| 157 | +- Unit-test composables as plain functions; component-test with Vue Test Utils / Testing Library. |
| 158 | +- Test observable behavior and rendered output, not internal implementation details. |
| 159 | +- Mock stores with `createTestingPinia`; stub router and async boundaries as needed. |
| 160 | +- Cover critical user journeys with end-to-end tests (Playwright or Cypress). |
| 161 | + |
| 162 | +## Tooling |
| 163 | +- Use Vite with the official Vue plugin; enable `vue-tsc` for type-checking in CI. |
| 164 | +- Use ESLint (`eslint-plugin-vue`) and Prettier; enable Volar/Vue official extension for the best DX. |
| 165 | +- Manage env via `import.meta.env` with `VITE_`-prefixed variables. |
| 166 | + |
| 167 | +## Anti-Patterns to Avoid |
| 168 | +- Mixing Options API and Composition API arbitrarily in the same codebase. |
| 169 | +- Mutating props directly, or destructuring `reactive()`/Pinia stores without `toRefs`/`storeToRefs`. |
| 170 | +- Using `watch` for values that should be `computed`. |
| 171 | +- `v-if` together with `v-for` on one element; using array index as `:key`. |
| 172 | +- Heavy logic inside templates; unbounded deep reactivity on large data. |
| 173 | +- Leaking timers/listeners by skipping `onUnmounted` cleanup. |
| 174 | +- Rendering untrusted HTML via `v-html`. |
0 commit comments