
Vue
- 4 installs
- 19 repo stars
- Updated August 1, 2026
- xobotyi/cc-foundry
Helps with ai & agent building tasks.
About
vue is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- vue
- AI & Agent Building
- AI-coding skill
Vue by the numbers
- 4 all-time installs (skills.sh)
- Ranked #13,372 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/xobotyi/cc-foundry --skill vueAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4 |
|---|---|
| repo stars | ★ 19 |
| Last updated | August 1, 2026 |
| Repository | xobotyi/cc-foundry ↗ |
What it does
Helps with ai & agent building tasks.
Files
Vue
Composition API is the default. `<script setup>` is the default syntax. If you reach for Options API, you need a reason.
Vue 3 rewards explicit, composable code. Prefer ref() over reactive(), composables over mixins, and typed props over runtime-only validation. References contain extended examples, rationale, and edge cases for each topic.
References
| Topic | Reference | Contents |
|---|---|---|
| Reactivity | [${CLAUDE_SKILL_DIR}/references/reactivity.md] | Ref unwrapping, watchers, computed edge cases |
| SFC | [${CLAUDE_SKILL_DIR}/references/sfc.md] | Full compiler macros catalog, scoped styles, template refs |
| Components | [${CLAUDE_SKILL_DIR}/references/components.md] | Props, emits, slots, provide/inject |
| Composables | [${CLAUDE_SKILL_DIR}/references/composables.md] | Design patterns, composition, restrictions |
| TypeScript | [${CLAUDE_SKILL_DIR}/references/typescript.md] | Full utility types table, generic components, event typing |
| Performance | [${CLAUDE_SKILL_DIR}/references/performance.md] | Update optimization, large lists, profiling |
Reactivity
Choosing a Reactive Primitive
| Primitive | Use when |
|---|---|
ref() | Default choice. Works with any value type. |
reactive() | Grouping related state when destructure is not needed. |
shallowRef() | Large immutable structures, external state integration. |
shallowReactive() | Root-level-only reactivity on objects. |
computed() | Derived state. Caches until dependencies change. |
ref() Is the Primary API
- Works with primitives (
string,number,boolean). - Can be destructured from composable returns without losing reactivity.
- Can be reassigned (
count.value = newValue). - Consistent
.valueaccess pattern everywhere in script. - Access
.valuein script, omit in template — templates auto-unwrap top-level refs.
reactive() Limitations
- Cannot hold primitives.
- Reassignment loses reactivity —
state = reactive({...})breaks tracking. - Destructuring primitives loses reactivity — use
toRefs()if you must destructure. - Do not use
reactive()as the primary primitive. Useref().
Ref Unwrapping Rules
- Top-level refs in templates are auto-unwrapped:
{{ count }}works. - Non-top-level refs in plain objects are NOT unwrapped:
{{ obj.id + 1 }}breaks
if obj.id is a ref. Destructure to top level to fix.
- Refs nested inside
reactive()objects are unwrapped automatically. - Refs inside reactive arrays/collections are NOT unwrapped — need
.value.
Computed Properties
- Keep computed getters pure — no side effects.
- Split complex computed into smaller ones.
- Computed caches its value; only recalculates when dependencies change.
- Computed stability (3.4+): only triggers effects when the returned value actually
changes. Avoid returning new objects from computed — each new object is "different".
- Writable computed is rare — use sparingly. Requires
get/setform.
Watchers
`watch()` vs `watchEffect()`:
Use watch() when | Use watchEffect() when |
|---|---|
| Need old and new values | Don't need old value |
| Want lazy execution | Want immediate execution |
| Watching specific sources | Dependencies are implicit |
| Need conditional watching | Effect tracks all accessed refs |
`watch()` options:
{ immediate: true }— run callback on creation (like watchEffect).{ deep: true }— watch all nested properties (expensive, use sparingly).- Watch a getter for specific property:
watch(() => obj.specificProp, callback). - Watch multiple sources:
watch([a, b], ([newA, newB]) => {...}).
Cleanup: Both watch and watchEffect support cleanup via the onCleanup parameter. Use it for aborting fetch requests, clearing timers, removing listeners.
DOM Update Timing
Reactive state changes batch DOM updates to the next tick. Use nextTick() for post-DOM-update logic.
Single-File Components
Block Order
Always: <script setup> first, <template> second, <style> last.
Organization Within <script setup>
Order declarations logically:
1. Imports — Vue APIs, components, composables, types 2. Props and emits — defineProps, defineEmits 3. Composable calls — useRouter(), custom composables 4. Reactive state — ref(), reactive(), computed() 5. Functions — event handlers, helpers 6. Watchers — watch(), watchEffect() 7. Lifecycle hooks — onMounted(), onUnmounted() 8. Expose — defineExpose() (rare)
Compiler Macros
Key macros available without import in <script setup>: defineProps(), defineEmits(), defineModel(). Use defineOptions() for options that <script setup> doesn't natively support (name, inheritAttrs: false). Full macro catalog in ${CLAUDE_SKILL_DIR}/references/sfc.md.
Template Syntax
Directive shorthands — use consistently, don't mix:
| Shorthand | Full | Purpose |
|---|---|---|
:prop | v-bind:prop | Bind attribute/prop |
@event | v-on:event | Listen to event |
#slot | v-slot:slot | Named slot |
Conditional rendering:
v-if/v-else-if/v-elsefor conditional blocks.v-showfor frequent toggles (CSSdisplay: none, avoids mount/unmount cost).- Use
v-iffor conditions that rarely change,v-showfor frequent toggles.
Template refs:
- 3.5+:
useTemplateRef<HTMLInputElement>('input')with matchingrefattribute. - Pre-3.5:
ref<HTMLInputElement | null>(null)with matching ref name.
Scoped Styles
- Use
<style scoped>by default. Global styles only inApp.vueor layouts. - Child component root elements are affected by both parent and child scoped styles.
- Deep selectors:
.parent :deep(.child-class)to style child internals (use sparingly). - CSS modules:
<style module>generates unique class names, accessed via$style. v-bind(color)in<style>uses reactive values in CSS.
Components
Naming
- Multi-word names always —
TodoItemnotItem. Avoids HTML element conflicts. - PascalCase in SFC templates:
<TodoItem />. Kebab-case only in in-DOM templates. - PascalCase filenames:
TodoItem.vue. - Base component prefix for presentational components:
BaseButton,BaseIcon. - Parent prefix for tightly coupled children:
TodoListItem,TodoListItemButton. - Highest-level word first to group related:
SearchButtonClear,SearchInputQuery. - Full words always — no abbreviations.
- One component per file. No inline registration.
- Self-closing tags for components without children:
<MyComponent />. - PascalCase when importing in JS/TS.
Props
- Use
defineProps<T>()(type-based) in TypeScript projects. Object syntax in JS
projects. Never array syntax in committed code.
- Declare in
camelCase, use in templates askebab-case— Vue converts automatically. - Never mutate props. Use
computed()for transformations,ref()+ initial value
for local copies.
- One-way data flow — props down, events up.
- Reactive props destructure (3.5+): destructured props are reactive, usable in
watch/computed directly.
- Pass destructured props to composables via getter:
useComposable(() => id).
Emits
- Declare all emits with
defineEmits()— preferably type-based. - Emit in
camelCase:emit('someEvent'). Listen inkebab-case:
@some-event="handler". Vue converts automatically.
- Use named tuple syntax (3.3+) for type-based emits:
defineEmits<{ change: [id: number] }>().
v-model
- Use
defineModel<T>()for two-way binding shorthand. - Named v-model:
defineModel<string>('firstName')with
v-model:first-name="first" on parent.
Slots
- Default slot:
<slot />in child, content between tags in parent. - Named slots:
<slot name="header" />in child,<template #header>in parent. - Scoped slots: pass data via slot props —
<slot :item="item" />, consume with
<template #default="{ item }">.
Provide / Inject
- Use
Symbolkeys (InjectionKey<T>) for type safety — avoid string collisions. - Export keys from a shared
keys.tsfile. - Provide
readonly()refs to prevent consumers from mutating state. - Provide updater functions when consumers need to change provided state.
- Always provide a default or handle
undefinedin the consumer.
Template Rules
v-foralways has:key— stable, unique identifiers.- Never
v-ifon same element asv-for. Usecomputedto filter, or wrap with
<template v-for>.
- Simple expressions only in templates. Move logic to
computedor functions. - Multi-attribute elements span multiple lines — one attribute per line.
Composables
Design
- Always prefix with
use:useMouse,useFetch,useAuth. camelCasenaming:useEventListenernotuse-event-listener.- The
useprefix signals the function uses Vue reactivity and must be called
within setup() or <script setup>.
Structure
Follow this order: (1) reactive state, (2) logic that modifies state, (3) lifecycle hooks for side effects, (4) return refs.
Return Values
Always return a plain object containing refs. Never return a reactive() object — it loses reactivity on destructure. If the consumer wants an object, they can wrap: const mouse = reactive(useMouse()).
Input Arguments
Accept refs, getters, and raw values. Use toValue() (with MaybeRefOrGetter<T> type) to normalize inputs inside watchEffect so reactive sources are tracked.
Side Effects and Cleanup
- Always clean up side effects in
onUnmounted()— event listeners, timers,
subscriptions.
- SSR safety: DOM-specific effects go in
onMounted()/onUnmounted(), not at
top level.
Composition
Composables can call other composables — build complex logic by composing simple hooks.
Usage Restrictions
- Must be called inside
<script setup>orsetup()function. - Must be called synchronously — not inside async callbacks or promises.
- Exception:
<script setup>restores the active instance afterawait, so composables
work after await in <script setup>.
- Inside lifecycle hooks like
onMounted()is acceptable.
Composables vs Alternatives
| Technique | Drawback |
|---|---|
| Mixins | Unclear property sources, namespace collisions, implicit coupling |
| Renderless components | Extra component instance overhead |
| Utility functions | No reactive state or lifecycle hooks |
Composables are explicit, namespaced through destructuring, and integrate with Vue's reactivity and lifecycle.
TypeScript
Props
- Use
defineProps<T>()with an interface. - Defaults with 3.5+ destructure:
const { title, count = 0 } = defineProps<Props>(). - Defaults with 3.4 and below:
withDefaults(defineProps<Props>(), { count: 0 }).
Mutable reference defaults (arrays, objects) must use factory functions with withDefaults.
- Imported types work since Vue 3.3.
Emits
Use named tuple syntax (3.3+): defineEmits<{ change: [id: number] }>().
Refs
- Refs infer types from initial values —
ref(0)isRef<number>. - Use explicit type for union types:
ref<string | number>('2024'). - Nullable:
ref<User | null>(null). - Without initial value:
ref<ResponseData>()yieldsRef<ResponseData | undefined>.
Computed
Types are inferred. Use explicit generic only when inference falls short: computed<string>(() => ...).
Reactive
Annotate with interface, do not use reactive<T>() generic — the returned type handles ref unwrapping differently from the generic parameter.
Template Refs
- 3.5+:
useTemplateRef<HTMLInputElement>('input'). - Component refs:
useTemplateRef<InstanceType<typeof MyComponent>>('comp'). - Pre-3.5:
ref<HTMLInputElement | null>(null).
Provide / Inject
Use InjectionKey<T> for type-safe keys. String keys require generic annotation: inject<string>('key').
Generic Components
Use generic attribute on <script setup>: <script setup lang="ts" generic="T extends string | number">.
Event Handlers
Type DOM events explicitly: (event: Event) then cast target: event.target as HTMLInputElement.
Utility Types
Use Vue's typed helpers: Ref<T>, ComputedRef<T>, MaybeRefOrGetter<T>, InjectionKey<T>, PropType<T>, ComponentPublicInstance. Full utility types table in ${CLAUDE_SKILL_DIR}/references/typescript.md.
Performance
Page Load
- Use dynamic imports for route-level code splitting.
- Use
defineAsyncComponent()for component-level splitting. - Prefer tree-shakable dependencies (
lodash-esoverlodash). <script setup>compiles to more minification-friendly code than Options API.
Update Performance
- Props stability — pass derived booleans (
active="item.id === activeId") instead
of raw IDs to prevent unnecessary child re-renders.
- `v-once` — render once, skip all future updates. For truly static content.
- `v-memo` — conditionally skip sub-tree updates. Accepts a dependency array; only
re-renders when a dependency changes. Use on v-for lists where most items don't change.
- Computed stability (3.4+) — computed only triggers effects when value actually
changes. Avoid returning new objects from computed (each new object is "different").
Large Lists
- Virtual scrolling for 1000+ items — render only visible items
(vue-virtual-scroller, vueuc/VVirtualList).
shallowRef()for large immutable datasets — mutations must replace the whole value.- Avoid unnecessary wrapper components in large lists — every component instance has
overhead.
Watch Performance
- Avoid
{ deep: true }on large objects — watch specific properties instead. - Throttle/debounce watchers for expensive callbacks (e.g.,
useDebounceFnfrom
VueUse).
Profiling
- Enable
app.config.performance = truefor Vue-specific performance markers. - Use Chrome DevTools Performance panel and Vue DevTools profiler.
- Measure before and after — don't optimize without data.
Application
When writing Vue code:
- Apply all conventions silently — don't narrate each rule being followed.
- If an existing codebase contradicts a convention, follow the codebase and
flag the divergence once.
- Always use
<script setup>unless there is a documented reason not to.
When reviewing Vue code:
- Cite the specific violation and show the fix inline.
- Don't lecture or quote the rule — state what's wrong and how to fix it.
Integration
This skill provides Vue-specific conventions. The coding skill governs workflow; language skills govern JS/TS choices; this skill governs component architecture, reactivity patterns, and framework API usage.
{
"sources": {
"Vue Style Guide - Overview": "https://raw.githubusercontent.com/vuejs/docs/main/src/style-guide/index.md",
"Vue Style Guide - Essential Rules": "https://raw.githubusercontent.com/vuejs/docs/main/src/style-guide/rules-essential.md",
"Vue Style Guide - Strongly Recommended Rules": "https://raw.githubusercontent.com/vuejs/docs/main/src/style-guide/rules-strongly-recommended.md",
"Vue Composition API FAQ": "https://raw.githubusercontent.com/vuejs/docs/main/src/guide/extras/composition-api-faq.md",
"Vue Reactivity Fundamentals": "https://raw.githubusercontent.com/vuejs/docs/main/src/guide/essentials/reactivity-fundamentals.md",
"Vue Reactivity In Depth": "https://raw.githubusercontent.com/vuejs/docs/main/src/guide/extras/reactivity-in-depth.md",
"Vue Composables": "https://raw.githubusercontent.com/vuejs/docs/main/src/guide/reusability/composables.md",
"Vue Single-File Components": "https://raw.githubusercontent.com/vuejs/docs/main/src/guide/scaling-up/sfc.md",
"Vue Component Props": "https://raw.githubusercontent.com/vuejs/docs/main/src/guide/components/props.md",
"Vue Component Events": "https://raw.githubusercontent.com/vuejs/docs/main/src/guide/components/events.md",
"Vue Provide-Inject": "https://raw.githubusercontent.com/vuejs/docs/main/src/guide/components/provide-inject.md",
"Vue Performance": "https://raw.githubusercontent.com/vuejs/docs/main/src/guide/best-practices/performance.md",
"Vue TypeScript with Composition API": "https://raw.githubusercontent.com/vuejs/docs/main/src/guide/typescript/composition-api.md"
},
"lastFetched": "2026-02-16T15:43:03.196Z"
}
Components
Vue components encapsulate template, logic, and styles. This reference covers props, emits, slots, provide/inject, and naming conventions.
Props
Declaration
Always use detailed prop definitions. Array syntax is for prototyping only.
<script setup lang="ts">
// Type-based (preferred in TS projects)
const props = defineProps<{
title: string
count?: number
items: string[]
}>()
// With defaults
const { title, count = 0 } = defineProps<{
title: string
count?: number
}>()
</script>For JS projects, use object syntax:
<script setup>
const props = defineProps({
title: {
type: String,
required: true
},
count: {
type: Number,
default: 0
}
})
</script>Reactive Props Destructure (3.5+)
Destructured props are reactive in 3.5+:
const { title, count = 0 } = defineProps<{
title: string
count?: number
}>()
// `title` is reactive — usable in watch/computed
watch(() => title, (newTitle) => { /* ... */ })Pass destructured props to composables via getter:
const { id } = defineProps<{ id: number }>()
// Wrap in getter to retain reactivity
useComposable(() => id)One-Way Data Flow
Props flow down, events flow up. Never mutate props directly.
// WRONG
props.count++
// RIGHT: local copy for initial value
const localCount = ref(props.count)
// RIGHT: computed for transformations
const normalizedTitle = computed(() => props.title.trim().toLowerCase())Prop Naming
- Declare in
camelCase:greetingText - Use in templates as
kebab-case::greeting-text="value" - This is Vue's automatic case conversion
Emits
Declaration
<script setup lang="ts">
// Type-based (preferred)
const emit = defineEmits<{
change: [id: number]
update: [value: string]
}>()
// Runtime validation
const emit = defineEmits({
submit: (payload: { email: string }) => {
return !!payload.email // validation
}
})
</script>Usage
function handleClick() {
emit('change', item.id)
}In templates, use $emit:
<template>
<button @click="$emit('change', item.id)">Update</button>
</template>Event Naming
- Emit in
camelCase:emit('someEvent') - Listen in
kebab-case:@some-event="handler" - Vue handles the conversion automatically
v-model
Basic
<!-- Parent -->
<CustomInput v-model="searchText" />
<!-- CustomInput.vue -->
<script setup lang="ts">
const model = defineModel<string>()
</script>
<template>
<input :value="model" @input="model = $event.target.value" />
</template>Named v-model
<!-- Parent -->
<UserForm v-model:first-name="first" v-model:last-name="last" />
<!-- UserForm.vue -->
<script setup lang="ts">
const firstName = defineModel<string>('firstName')
const lastName = defineModel<string>('lastName')
</script>Slots
Default Slot
<!-- Parent -->
<AlertBox>Something happened.</AlertBox>
<!-- AlertBox.vue -->
<template>
<div class="alert">
<slot />
</div>
</template>Named Slots
<!-- Parent -->
<BaseLayout>
<template #header>
<h1>Page Title</h1>
</template>
<template #default>
<p>Main content</p>
</template>
<template #footer>
<p>Footer</p>
</template>
</BaseLayout>
<!-- BaseLayout.vue -->
<template>
<header><slot name="header" /></header>
<main><slot /></main>
<footer><slot name="footer" /></footer>
</template>Scoped Slots
<!-- ItemList.vue -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot name="item" :item="item" :index="index" />
</li>
</ul>
</template>
<!-- Parent -->
<ItemList :items="items">
<template #item="{ item, index }">
<span>{{ index }}: {{ item.name }}</span>
</template>
</ItemList>Provide / Inject
Dependency injection for deep component trees. Avoids prop drilling.
Providing
<script setup lang="ts">
import { provide, ref, readonly } from 'vue'
import type { InjectionKey } from 'vue'
// Use Symbol keys for type safety
export const ThemeKey: InjectionKey<Ref<string>> = Symbol('theme')
const theme = ref('dark')
provide(ThemeKey, readonly(theme))
// Provide updater functions instead of mutable state
function updateTheme(newTheme: string) {
theme.value = newTheme
}
provide('updateTheme', updateTheme)
</script>Injecting
<script setup lang="ts">
import { inject } from 'vue'
import { ThemeKey } from './keys'
const theme = inject(ThemeKey) // Ref<string> | undefined
const theme = inject(ThemeKey, ref('light')) // with default
</script>Rules
- Use `Symbol` keys for non-trivial apps to avoid collisions.
- Export keys from a shared `keys.ts` file.
- Provide `readonly()` refs to prevent consumers from mutating state.
- Provide updater functions when consumers need to change state.
- Always provide a default or handle
undefinedin the consumer.
Component Naming
File Names
- PascalCase preferred:
TodoItem.vue,BaseButton.vue - kebab-case acceptable:
todo-item.vue,base-button.vue - Choose one convention for the project and be consistent.
In Templates
<!-- PascalCase in SFC templates (preferred) -->
<TodoItem />
<!-- kebab-case in in-DOM templates (required) -->
<todo-item></todo-item>Naming Patterns
| Pattern | Example | When |
|---|---|---|
| Multi-word | TodoItem | Always (except root App) |
| Base prefix | BaseButton, BaseIcon | Presentational components |
| Parent prefix | TodoListItem, TodoListItemButton | Tightly coupled children |
| Highest-level first | SearchButtonClear, SearchInputQuery | Groups related components |
| Full words | StudentDashboardSettings | Always — no abbreviations |
In JS/TS
// Always PascalCase when importing
import TodoItem from './TodoItem.vue'Composables
Composables are functions that encapsulate and reuse stateful logic using the Composition API. They replace mixins entirely.
Naming
- Always prefix with `use`:
useMouse,useFetch,useAuth. - camelCase:
useEventListener, notuse-event-listener. - The
useprefix signals that the function uses Vue reactivity and
must be called within setup() or <script setup>.
Structure
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
// 1. Reactive state
const x = ref(0)
const y = ref(0)
// 2. Logic that modifies state
function update(event: MouseEvent) {
x.value = event.pageX
y.value = event.pageY
}
// 3. Lifecycle hooks for side effects
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// 4. Return refs (not reactive object)
return { x, y }
}Return Values
Always return a plain object containing refs. This allows destructuring with retained reactivity:
// GOOD: plain object with refs
return { x, y, isLoading, error }
// BAD: reactive object (loses reactivity on destructure)
return reactive({ x, y, isLoading, error })If the consumer wants an object, they can wrap:
const mouse = reactive(useMouse()) // mouse.x auto-unwrapsInput Arguments
Accept refs, getters, and raw values. Use toValue() to normalize:
import { ref, watchEffect, toValue, type MaybeRefOrGetter } from 'vue'
export function useFetch(url: MaybeRefOrGetter<string>) {
const data = ref<unknown>(null)
const error = ref<Error | null>(null)
watchEffect(() => {
data.value = null
error.value = null
fetch(toValue(url))
.then(res => res.json())
.then(json => { data.value = json })
.catch(err => { error.value = err })
})
return { data, error }
}Calling toValue() inside watchEffect ensures the reactive source is tracked.
Usage:
// All three work:
useFetch('/api/users') // raw string
useFetch(urlRef) // ref
useFetch(() => `/api/users/${id}`) // getterSide Effects
Cleanup
Always clean up side effects in onUnmounted():
export function useEventListener(
target: EventTarget,
event: string,
handler: EventListener
) {
onMounted(() => target.addEventListener(event, handler))
onUnmounted(() => target.removeEventListener(event, handler))
}SSR Safety
DOM-specific side effects go in post-mount hooks:
export function useWindowSize() {
const width = ref(0)
const height = ref(0)
function update() {
width.value = window.innerWidth
height.value = window.innerHeight
}
// Only runs in browser, not during SSR
onMounted(() => {
update()
window.addEventListener('resize', update)
})
onUnmounted(() => {
window.removeEventListener('resize', update)
})
return { width, height }
}Composable Composition
Composables can call other composables:
import { ref } from 'vue'
import { useEventListener } from './useEventListener'
export function useMouse() {
const x = ref(0)
const y = ref(0)
useEventListener(window, 'mousemove', (event: MouseEvent) => {
x.value = event.pageX
y.value = event.pageY
})
return { x, y }
}Usage Restrictions
Composables must be called:
- Inside
<script setup>(most common) - Inside the
setup()function - Synchronously (not inside async callbacks or promises)
- Inside lifecycle hooks like
onMounted()is acceptable
Exception: <script setup> restores the active instance after await, so composables work after await in <script setup>.
Composables vs Alternatives
| Technique | Drawback |
|---|---|
| Mixins | Unclear property sources, namespace collisions, implicit coupling |
| Renderless components | Extra component instance overhead |
| Utility functions | No reactive state or lifecycle hooks |
Composables solve all three — they're explicit, namespaced through destructuring, and integrate with Vue's reactivity and lifecycle.
Performance
Vue is fast by default. Optimize only when profiling reveals a bottleneck. Premature optimization creates complexity without measurable benefit.
Page Load
Code Splitting
Use dynamic imports for route-level code splitting:
// router.ts
const UserProfile = () => import('./views/UserProfile.vue')
const routes = [
{ path: '/user/:id', component: UserProfile }
]Use defineAsyncComponent for component-level splitting:
import { defineAsyncComponent } from 'vue'
const HeavyChart = defineAsyncComponent(
() => import('./components/HeavyChart.vue')
)Bundle Size
- Prefer tree-shakable dependencies (
lodash-esoverlodash). - Vue APIs are tree-shakable — unused features like
<Transition>are
excluded from production builds.
- Use
<script setup>— it compiles to more minification-friendly code
than Options API.
Update Performance
Props Stability
Avoid passing values that change for every item:
<!-- BAD: every ListItem re-renders when activeId changes -->
<ListItem
v-for="item in list"
:key="item.id"
:id="item.id"
:active-id="activeId"
/>
<!-- GOOD: only items whose active status changed re-render -->
<ListItem
v-for="item in list"
:key="item.id"
:id="item.id"
:active="item.id === activeId"
/>v-once
Render once, skip all future updates:
<span v-once>{{ expensiveComputation }}</span>Use for truly static content that depends on initial data.
v-memo
Conditionally skip sub-tree updates:
<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
<p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p>
<!-- expensive sub-tree -->
</div>v-memo accepts a dependency array. The sub-tree only re-renders when a dependency changes. Use on v-for lists where most items don't change.
Computed Stability (3.4+)
Computed properties only trigger effects when the returned value actually changes:
const isEven = computed(() => count.value % 2 === 0)
// Changing count from 2 to 4 does NOT re-trigger watchers
// because isEven stays trueAvoid returning new objects from computed — each new object is "different":
// BAD: new object every time, always triggers
const result = computed(() => ({
isEven: count.value % 2 === 0
}))
// GOOD: return old value when nothing changed
const result = computed((oldValue) => {
const newValue = { isEven: count.value % 2 === 0 }
if (oldValue && oldValue.isEven === newValue.isEven) {
return oldValue
}
return newValue
})Large Lists
Virtual Scrolling
For lists with 1000+ items, use virtual scrolling — render only visible items:
// Community libraries:
// - vue-virtual-scroller
// - vueuc/VVirtualListshallowRef for Large Data
Opt out of deep reactivity for large, immutable datasets:
import { shallowRef } from 'vue'
const largeList = shallowRef(fetchedData)
// Mutations must replace the whole value
largeList.value = [...largeList.value, newItem]Component Abstractions
Every component instance has overhead (state, lifecycle, rendering). In large lists, avoid unnecessary wrapper components:
<!-- BAD: 100 wrapper components in a list -->
<ItemWrapper v-for="item in items" :key="item.id">
<ItemContent :item="item" />
</ItemWrapper>
<!-- GOOD: flatten when wrapper adds no value -->
<ItemContent v-for="item in items" :key="item.id" :item="item" />Watch Performance
Avoid Deep Watching Large Objects
// BAD: watches every nested property change
watch(largeObject, callback, { deep: true })
// GOOD: watch specific properties
watch(() => largeObject.value.specificProp, callback)Throttle/Debounce Watchers
import { watchEffect } from 'vue'
import { useDebounceFn } from '@vueuse/core'
const debouncedSearch = useDebounceFn((query: string) => {
fetchResults(query)
}, 300)
watch(searchQuery, (query) => {
debouncedSearch(query)
})Profiling
1. Enable Vue-specific performance markers:
app.config.performance = true2. Use Chrome DevTools Performance panel. 3. Use Vue DevTools performance profiler. 4. Measure before and after — don't optimize without data.
Reactivity
Vue's reactivity system tracks dependencies at property-access granularity. Understanding it prevents the most common Vue bugs.
Choosing a Reactive Primitive
| Primitive | Use When |
|---|---|
ref() | Default choice. Works with any value type. |
reactive() | Grouping related state into one object when destructure is not needed. |
shallowRef() | Large immutable structures, external state integration. |
shallowReactive() | Root-level-only reactivity on objects. |
computed() | Derived state. Caches until dependencies change. |
ref() is the Primary API
import { ref } from 'vue'
const count = ref(0)
// In script: access .value
count.value++
// In template: auto-unwrapped
// {{ count }} renders "1"Why `ref()` over `reactive()`:
- Works with primitives (
string,number,boolean) - Can be destructured from composable returns without losing reactivity
- Can be reassigned (
count.value = newValue) - Consistent
.valueaccess pattern everywhere in script
reactive() Limitations
import { reactive } from 'vue'
const state = reactive({ count: 0, name: 'Vue' })
// Works: property access is tracked
state.count++
// BREAKS: reassignment loses reactivity
// state = reactive({ count: 1, name: 'Vue' }) // Don't do this
// BREAKS: destructuring primitives loses reactivity
// const { count } = state // count is now a plain numberUse toRefs() if you must destructure a reactive object:
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = toRefs(state) // count and name are Ref<>Computed Properties
import { ref, computed } from 'vue'
const items = ref([1, 2, 3, 4, 5])
// Read-only computed
const evenItems = computed(() => items.value.filter(n => n % 2 === 0))
// Writable computed (rare — use sparingly)
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (val) => {
const [first, last] = val.split(' ')
firstName.value = first
lastName.value = last
}
})Rules:
- Keep computed getters pure — no side effects.
- Split complex computed into smaller ones.
- Computed caches its value; only recalculates when dependencies change.
- Computed stability (3.4+): only triggers effects when the returned value
actually changes.
Watchers
watch()
Watches specific sources. Lazy by default (doesn't run on mount).
import { ref, watch } from 'vue'
const query = ref('')
// Watch a single ref
watch(query, (newVal, oldVal) => {
fetchResults(newVal)
})
// Watch a getter
watch(
() => props.id,
(newId) => { fetchData(newId) }
)
// Watch multiple sources
watch([firstName, lastName], ([newFirst, newLast]) => {
updateFullName(newFirst, newLast)
})
// Immediate execution
watch(source, callback, { immediate: true })
// Deep watching (use sparingly — expensive)
watch(source, callback, { deep: true })watchEffect()
Runs immediately, auto-tracks all reactive dependencies accessed during execution.
import { ref, watchEffect } from 'vue'
const url = ref('/api/data')
watchEffect(() => {
// Automatically tracks `url`
fetch(url.value).then(/* ... */)
})When to use which:
Use watch() when | Use watchEffect() when |
|---|---|
| Need old and new values | Don't need old value |
| Want lazy execution | Want immediate execution |
| Watching specific sources | Dependencies are implicit |
| Need conditional watching | Effect tracks all accessed refs |
Cleanup
Both watch and watchEffect support cleanup via the onCleanup parameter:
watchEffect((onCleanup) => {
const controller = new AbortController()
fetch(url.value, { signal: controller.signal })
onCleanup(() => controller.abort())
})Deep Reactivity
ref() and reactive() are deep by default — nested objects are also reactive:
const obj = ref({
nested: { count: 0 },
arr: ['foo', 'bar']
})
// These trigger updates:
obj.value.nested.count++
obj.value.arr.push('baz')Use shallowRef() to opt out of deep reactivity for large structures:
import { shallowRef, triggerRef } from 'vue'
const data = shallowRef({ items: largeArray })
// This does NOT trigger updates:
data.value.items.push(newItem)
// This does:
data.value = { ...data.value, items: [...data.value.items, newItem] }
// Or manually trigger:
data.value.items.push(newItem)
triggerRef(data)Ref Unwrapping
In Templates
Top-level refs are auto-unwrapped in templates:
const count = ref(0)
// Template: {{ count }} renders "0", not the ref objectNon-top-level refs in plain objects are NOT unwrapped:
const obj = { id: ref(1) }
// Template: {{ obj.id + 1 }} renders "[object Object]1"
// Fix: const { id } = obj // destructure to top levelIn Reactive Objects
Refs nested in reactive() objects are unwrapped:
const count = ref(0)
const state = reactive({ count })
console.log(state.count) // 0, not Ref<0>Refs in reactive arrays/collections are NOT unwrapped:
const books = reactive([ref('Vue Guide')])
console.log(books[0].value) // Need .value hereDOM Update Timing
Reactive state changes batch DOM updates to the next tick:
import { ref, nextTick } from 'vue'
const count = ref(0)
count.value++
// DOM not yet updated here
await nextTick()
// DOM is now updatedSingle-File Components
SFCs (.vue files) colocate template, logic, and styles in one file. They are the recommended authoring format for all non-trivial Vue code.
Block Order
Always order blocks consistently:
<script setup lang="ts">
// logic
</script>
<template>
<!-- markup -->
</template>
<style scoped>
/* styles */
</style>`<script setup>` first, `<template>` second, `<style>` last.
<script setup>
Everything declared at the top level is available in the template. No need for return statements or components registration.
<script setup lang="ts">
import { ref, computed } from 'vue'
import TodoItem from './TodoItem.vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
</script>
<template>
<TodoItem />
<button @click="increment">{{ count }} ({{ doubled }})</button>
</template>Organization Within <script setup>
Order declarations logically:
1. Imports — Vue APIs, components, composables, types 2. Props and emits — defineProps, defineEmits 3. Composable calls — useRouter(), useFetch(), custom composables 4. Reactive state — ref(), reactive(), computed() 5. Functions — event handlers, helpers 6. Watchers — watch(), watchEffect() 7. Lifecycle hooks — onMounted(), onUnmounted() 8. Expose — defineExpose() (rare)
Compiler Macros
These are available without import in <script setup>:
| Macro | Purpose |
|---|---|
defineProps() | Declare component props |
defineEmits() | Declare component events |
defineExpose() | Expose public instance properties |
defineOptions() | Set component options (name, inheritAttrs) |
defineSlots() | Type-check slot props (TS only) |
defineModel() | Two-way binding (v-model) shorthand |
withDefaults() | Set defaults for type-based props |
defineOptions()
Set options that <script setup> doesn't natively support:
<script setup lang="ts">
defineOptions({
name: 'CustomName',
inheritAttrs: false
})
</script>Template Syntax
Directives
| Shorthand | Full | Purpose |
|---|---|---|
:prop | v-bind:prop | Bind attribute/prop |
@event | v-on:event | Listen to event |
#slot | v-slot:slot | Named slot |
Always use shorthands consistently — don't mix : and v-bind:.
Multi-Attribute Formatting
<!-- Bad: everything on one line -->
<MyComponent foo="a" bar="b" baz="c" />
<!-- Good: one attribute per line -->
<MyComponent
foo="a"
bar="b"
baz="c"
/>Conditional Rendering
<!-- v-if / v-else-if / v-else for conditional blocks -->
<div v-if="type === 'A'">A</div>
<div v-else-if="type === 'B'">B</div>
<div v-else>Other</div>
<!-- v-show for frequent toggling (CSS display: none) -->
<div v-show="isVisible">Toggled frequently</div>Use v-if for conditions that rarely change. Use v-show for frequent toggles (avoids mount/unmount cost).
List Rendering
<!-- Always provide :key -->
<ul>
<li
v-for="item in items"
:key="item.id"
>
{{ item.name }}
</li>
</ul>
<!-- Filter with computed, not v-if on same element -->
<ul>
<li
v-for="user in activeUsers"
:key="user.id"
>
{{ user.name }}
</li>
</ul>Template Refs
<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'
const inputEl = useTemplateRef<HTMLInputElement>('input')
onMounted(() => {
inputEl.value?.focus()
})
</script>
<template>
<input ref="input" />
</template>Pre-3.5, use ref<HTMLInputElement | null>(null) with matching ref name.
Scoped Styles
<style scoped>
.button {
color: red;
}
</style>Scoped styles use data attributes to scope CSS to the component. Child component root elements are affected by both parent and child scoped styles.
Deep Selectors
To style child component internals from a parent (use sparingly):
<style scoped>
.parent :deep(.child-class) {
color: blue;
}
</style>CSS Modules
Alternative to scoped styles — generates unique class names:
<template>
<button :class="$style.button">Click</button>
</template>
<style module>
.button {
color: red;
}
</style>v-bind() in Styles
Use reactive values in CSS:
<script setup>
import { ref } from 'vue'
const color = ref('red')
</script>
<style scoped>
.text {
color: v-bind(color);
}
</style>TypeScript
Vue 3 Composition API is designed for TypeScript. Most types are inferred automatically — add explicit annotations only where inference falls short.
Props
Type-Based Declaration (Preferred)
<script setup lang="ts">
interface Props {
title: string
count?: number
items: string[]
}
const props = defineProps<Props>()
</script>With Defaults
// 3.5+ reactive destructure (preferred)
const { title, count = 0, items = [] } = defineProps<Props>()
// 3.4 and below: withDefaults
const props = withDefaults(defineProps<Props>(), {
count: 0,
items: () => [] // factory for objects/arrays
})Note: Mutable reference type defaults (arrays, objects) must use factory functions with withDefaults to avoid shared references. This is NOT needed with destructure defaults.
Complex Prop Types
<script setup lang="ts">
interface Book {
title: string
author: string
year: number
}
const props = defineProps<{
book: Book
onSelect?: (book: Book) => void
}>()
</script>Imported Types
Works since Vue 3.3:
<script setup lang="ts">
import type { User } from '@/types'
const props = defineProps<{
user: User
}>()
</script>Emits
Type-Based Declaration
<script setup lang="ts">
// Named tuple syntax (3.3+, preferred)
const emit = defineEmits<{
change: [id: number]
update: [value: string]
}>()
// Call-signature syntax (also valid)
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
</script>Refs
Refs infer types from initial values:
const count = ref(0) // Ref<number>
const name = ref('Vue') // Ref<string>Explicit type when inference is insufficient:
// Union type
const year = ref<string | number>('2024')
// Nullable
const user = ref<User | null>(null)
// Without initial value
const data = ref<ResponseData>() // Ref<ResponseData | undefined>Computed
const count = ref(0)
const doubled = computed(() => count.value * 2) // ComputedRef<number>
// Explicit generic (rarely needed)
const result = computed<string>(() => {
return count.value > 0 ? 'positive' : 'zero'
})Reactive
interface State {
count: number
user: User | null
}
// Use interface annotation, not generic
const state: State = reactive({
count: 0,
user: null
})Don't use reactive<T>() generic — the returned type handles ref unwrapping differently from the generic parameter.
Template Refs
<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'
// 3.5+: auto-inferred from matching ref attribute
const input = useTemplateRef<HTMLInputElement>('input')
onMounted(() => {
input.value?.focus()
})
</script>
<template>
<input ref="input" />
</template>Pre-3.5:
const input = ref<HTMLInputElement | null>(null)Component Refs
import MyComponent from './MyComponent.vue'
type MyComponentInstance = InstanceType<typeof MyComponent>
const comp = useTemplateRef<MyComponentInstance>('comp')Provide / Inject
Use InjectionKey for type-safe provide/inject:
// keys.ts
import type { InjectionKey, Ref } from 'vue'
export const CountKey: InjectionKey<Ref<number>> = Symbol('count')// Provider
import { provide, ref } from 'vue'
import { CountKey } from './keys'
const count = ref(0)
provide(CountKey, count) // type-checked// Consumer
import { inject } from 'vue'
import { CountKey } from './keys'
const count = inject(CountKey) // Ref<number> | undefined
const count = inject(CountKey, ref(0)) // Ref<number>String keys require generic annotation:
const value = inject<string>('key') // string | undefined
const value = inject<string>('key', 'default') // stringEvent Handlers
Type DOM events explicitly:
function handleChange(event: Event) {
const target = event.target as HTMLInputElement
console.log(target.value)
}Generic Components
<script setup lang="ts" generic="T extends string | number">
const props = defineProps<{
items: T[]
selected: T
}>()
const emit = defineEmits<{
select: [item: T]
}>()
</script>Utility Types
| Type | Purpose |
|---|---|
Ref<T> | Ref wrapper type |
ComputedRef<T> | Computed ref type |
MaybeRef<T> | `T \ |
MaybeRefOrGetter<T> | `T \ |
InjectionKey<T> | Typed injection key |
PropType<T> | Runtime prop type casting |
ComponentPublicInstance | Generic component instance |