Vue 3 Composition API
Full Reference: See advanced.md for WebSocket composable, provide/inject plugin pattern, Socket.IO integration, and room management.
Deep Knowledge: Use mcp__documentation__fetch_docs with technology: vue for comprehensive documentation.
When NOT to Use This Skill
Skip this skill when:
- Working with Vue 2 Options API (use legacy Vue docs)
- Building React applications (use
frontend-react) - Using Angular framework (use
angular) - Working with Svelte (use
svelte) - Dealing with server-side only logic (no framework needed)
Component Structure
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
interface Props {
title: string
count?: number
}
const props = defineProps<Props>()
const emit = defineEmits<{
update: [value: string]
}>()
const localState = ref('')
const doubled = computed(() => props.count * 2)
onMounted(() => {
console.log('Component mounted')
})
</script>
<template>
<div>
<h1>{{ title }}</h1>
<p>{{ doubled }}</p>
</div>
</template>
Reactivity System
| API | Purpose |
|---|
ref() | Primitive reactive value |
reactive() | Reactive object |
computed() | Derived state |
watch() | Watch reactive sources |
watchEffect() | Auto-track dependencies |
Composables Pattern
// useCounter.ts
export function useCounter(initial = 0) {
const count = ref(initial)
const increment = () => count.value++
const decrement = () => count.value--
return { count, increment, decrement }
}
Key Concepts
<script setup> is recommended syntax- Use
ref for primitives, reactive for objects v-model for two-way binding- Slots for content distribution
Anti-Patterns
| Anti-Pattern | Why It's Bad | Correct Approach |
|---|
Using reactive() for primitives | Loses reactivity on destructure | Use ref() for primitives |
| Mutating props directly | Breaks one-way data flow | Emit events, use v-model |
Using v-html without sanitization | XSS vulnerability | Use DOMPurify before rendering |
| Large computed without memo | Recalculates on every render | Break into smaller computeds |
Not cleaning up in onUnmounted | Memory leaks | Clear timers, unsubscribe |
Using watch when computed suffices | Unnecessary complexity | Use computed for derived state |
Quick Troubleshooting
| Issue | Likely Cause | Solution |
|---|
| Computed not updating | Forgot .value on ref | Access refs with .value |
| Template not reactive | Used let instead of ref | Convert to ref() or reactive() |
| Props mutation warning | Directly modifying props | Clone props or emit update event |
| Component not re-rendering | Using reactive on primitive | Use ref() for primitives |
| Memory leaks | Forgot to cleanup | Add cleanup in onUnmounted |
v-model not working | Wrong event name | Use update:modelValue event |
Production Readiness
Security Best Practices
<script setup lang="ts">
// NEVER use v-html with user input without sanitization
import DOMPurify from 'dompurify'
const props = defineProps<{ userContent: string }>()
const sanitizedContent = computed(() => DOMPurify.sanitize(props.userContent))
</script>
<template>
<!-- BAD -->
<div v-html="userContent" />
<!-- GOOD -->
<div v-html="sanitizedContent" />
</template>
// Validate external URLs
const isValidUrl = (url: string): boolean => {
try {
const parsed = new URL(url)
return ['http:', 'https:'].includes(parsed.protocol)
} catch {
return false
}
}
// Never expose secrets in client code
// Use runtime config or server routes instead
const config = useRuntimeConfig()
// config.public.* is safe for client
// config.* (without public) stays server-side
Error Handling
<script setup lang="ts">
import { onErrorCaptured } from 'vue'
// Component-level error boundary
onErrorCaptured((error, instance, info) => {
// Log to error tracking service
logError(error, { component: instance?.$options.name, info })
// Return false to prevent error propagation
return false
})
</script>
// Global error handler (main.ts)
const app = createApp(App)
app.config.errorHandler = (error, instance, info) => {
console.error('Global error:', error)
// Send to error tracking (Sentry, etc.)
captureException(error, { extra: { info } })
}
app.config.warnHandler = (msg, instance, trace) => {
// Log warnings in development
if (import.meta.env.DEV) console.warn(msg, trace)
}
Performance Optimization
<script setup lang="ts">
import { defineAsyncComponent, shallowRef } from 'vue'
// Lazy load heavy components
const HeavyChart = defineAsyncComponent({
loader: () => import('./HeavyChart.vue'),
loadingComponent: LoadingSpinner,
delay: 200,
errorComponent: ErrorDisplay,
})
// Use shallowRef for large objects that don't need deep reactivity
const largeDataset = shallowRef<DataItem[]>([])
// Computed with getter/setter for derived state
const filteredItems = computed(() =>
items.value.filter(item => item.active)
)
</script>
<template>
<!-- Use v-once for static content -->
<header v-once>
<h1>{{ appTitle }}</h1>
</header>
<!-- Use v-memo for expensive list items -->
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.updated]">
<ExpensiveComponent :data="item" />
</div>
<!-- Virtual scrolling for large lists -->
<VirtualList :items="largeDataset" :item-height="50" />
</template>
Accessibility (a11y)
<template>
<!-- Use semantic HTML -->
<button @click="handleClick">Submit</button>
<!-- ARIA for dynamic content -->
<div role="alert" aria-live="polite" v-if="error">
{{ error }}
</div>
<!-- Focus management -->
<dialog ref="dialogRef" @vue:mounted="dialogRef?.focus()">
<h2 id="dialog-title">Confirm Action</h2>
<div aria-labelledby="dialog-title">...</div>
</dialog>
</template>
Testing Setup
// Component testing with Vue Test Utils
import { mount } from '@vue/test-utils'
import { describe, it, expect, vi } from 'vitest'
describe('UserForm', () => {
it('emits submit with form data', async () => {
const wrapper = mount(UserForm)
await wrapper.find('input[name="email"]').setValue('test@example.com')
await wrapper.find('form').trigger('submit')
expect(wrapper.emitted('submit')).toBeTruthy()
expect(wrapper.emitted('submit')[0]).toEqual([{ email: 'test@example.com' }])
})
})
Monitoring Metrics
| Metric | Alert Threshold |
|---|
| Largest Contentful Paint (LCP) | > 2.5s |
| First Input Delay (FID) | > 100ms |
| Cumulative Layout Shift (CLS) | > 0.1 |
| JavaScript bundle size | > 200KB (gzipped) |
| Component render time | > 16ms |
Build Optimization
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vue: ['vue', 'vue-router', 'pinia'],
ui: ['@headlessui/vue', '@vueuse/core'],
},
},
},
sourcemap: true,
},
})
Checklist
- Global error handler configured
- No sensitive data in client state
- DOMPurify for v-html content
- Async components for code splitting
- shallowRef for large non-reactive data
- v-memo for expensive list rendering
- Virtual scrolling for long lists
- Semantic HTML and ARIA labels
- Core Web Vitals monitored
- Bundle size optimized
- Error reporting service integrated
Reference Documentation
Deep Knowledge: Use mcp__documentation__fetch_docs with technology: vue for comprehensive documentation.