Agent skill
vuejs-development
Comprehensive Vue.js development skill covering Composition API, reactivity system, components, directives, and modern Vue 3 patterns based on official Vue.js documentation
Install this agent skill to your Project
npx add-skill https://github.com/manutej/luxor-claude-marketplace/tree/main/plugins/luxor-frontend-essentials/skills/vuejs-development
SKILL.md
Vue.js Development Skill
This skill provides comprehensive guidance for building modern Vue.js applications using the Composition API, reactivity system, single-file components, directives, and lifecycle hooks based on official Vue.js documentation.
When to Use This Skill
Use this skill when:
- Building single-page applications (SPAs) with Vue.js
- Creating progressive web applications (PWAs) with Vue
- Developing interactive user interfaces with reactive data
- Building component-based architectures
- Implementing forms, data fetching, and state management
- Creating reusable UI components and libraries
- Migrating from Options API to Composition API
- Optimizing Vue application performance
- Building accessible and maintainable web applications
- Integrating with TypeScript for type-safe development
Core Concepts
Reactivity System
Vue's reactivity system is the core mechanism that tracks dependencies and automatically updates the DOM when data changes.
Reactive State with ref():
import { ref } from 'vue'
// ref creates a reactive reference to a value
const count = ref(0)
// Access value with .value
console.log(count.value) // 0
// Modify value
count.value++
console.log(count.value) // 1
// In templates, .value is automatically unwrapped
// <template>{{ count }}</template>
Reactive Objects with reactive():
import { reactive } from 'vue'
// reactive creates a reactive proxy of an object
const state = reactive({
name: 'Vue',
version: 3,
features: ['Composition API', 'Teleport', 'Suspense']
})
// Access and modify properties directly
console.log(state.name) // 'Vue'
state.name = 'Vue.js'
// Nested objects are also reactive
state.features.push('Fragments')
When to Use ref() vs reactive():
// Use ref() for:
// - Primitive values (string, number, boolean)
// - Single values that need reactivity
const count = ref(0)
const message = ref('Hello')
const isActive = ref(true)
// Use reactive() for:
// - Objects with multiple properties
// - Complex data structures
const user = reactive({
id: 1,
name: 'John',
email: 'john@example.com',
preferences: {
theme: 'dark',
notifications: true
}
})
Computed Properties:
import { ref, computed } from 'vue'
const count = ref(0)
// Computed property automatically tracks dependencies
const doubled = computed(() => count.value * 2)
console.log(doubled.value) // 0
count.value = 5
console.log(doubled.value) // 10
// Writable computed
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(newValue) {
[firstName.value, lastName.value] = newValue.split(' ')
}
})
fullName.value = 'Jane Smith'
console.log(firstName.value) // 'Jane'
console.log(lastName.value) // 'Smith'
Watchers:
import { ref, watch, watchEffect } from 'vue'
const count = ref(0)
const message = ref('Hello')
// Watch a single ref
watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`)
})
// Watch multiple sources
watch([count, message], ([newCount, newMessage], [oldCount, oldMessage]) => {
console.log(`Count: ${newCount}, Message: ${newMessage}`)
})
// Watch reactive object property
const state = reactive({ count: 0 })
watch(
() => state.count,
(newValue, oldValue) => {
console.log(`State count changed from ${oldValue} to ${newValue}`)
}
)
// watchEffect automatically tracks dependencies
watchEffect(() => {
console.log(`Count is ${count.value}`)
// Automatically re-runs when count changes
})
// Immediate execution
watch(count, (newValue) => {
console.log(`Count is now ${newValue}`)
}, { immediate: true })
// Deep watching
const user = reactive({ profile: { name: 'John' } })
watch(user, (newValue) => {
console.log('User changed:', newValue)
}, { deep: true })
Composition API
The Composition API provides a set of function-based APIs for organizing component logic.
Basic Component with :
<script setup>
import { ref, computed, onMounted } from 'vue'
// Props
const props = defineProps({
title: String,
count: {
type: Number,
default: 0
}
})
// Emits
const emit = defineEmits(['update', 'delete'])
// Reactive state
const localCount = ref(props.count)
const message = ref('Hello Vue!')
// Computed
const doubledCount = computed(() => localCount.value * 2)
// Methods
function increment() {
localCount.value++
emit('update', localCount.value)
}
// Lifecycle
onMounted(() => {
console.log('Component mounted')
})
</script>
<template>
<div>
<h2>{{ title }}</h2>
<p>{{ message }}</p>
<p>Count: {{ localCount }}</p>
<p>Doubled: {{ doubledCount }}</p>
<button @click="increment">Increment</button>
</div>
</template>
Component without (verbose syntax):
<script>
import { ref, computed, onMounted } from 'vue'
export default {
name: 'MyComponent',
props: {
title: String,
count: {
type: Number,
default: 0
}
},
emits: ['update', 'delete'],
setup(props, { emit }) {
const localCount = ref(props.count)
const message = ref('Hello Vue!')
const doubledCount = computed(() => localCount.value * 2)
function increment() {
localCount.value++
emit('update', localCount.value)
}
onMounted(() => {
console.log('Component mounted')
})
return {
localCount,
message,
doubledCount,
increment
}
}
}
</script>
Single-File Components
Single-file components (.vue) combine template, script, and styles in one file.
Complete SFC Example:
<script setup>
import { ref, computed } from 'vue'
const tasks = ref([
{ id: 1, text: 'Learn Vue', completed: false },
{ id: 2, text: 'Build app', completed: false }
])
const newTaskText = ref('')
const completedCount = computed(() =>
tasks.value.filter(t => t.completed).length
)
const remainingCount = computed(() =>
tasks.value.filter(t => !t.completed).length
)
function addTask() {
if (newTaskText.value.trim()) {
tasks.value.push({
id: Date.now(),
text: newTaskText.value,
completed: false
})
newTaskText.value = ''
}
}
function toggleTask(id) {
const task = tasks.value.find(t => t.id === id)
if (task) task.completed = !task.completed
}
function removeTask(id) {
tasks.value = tasks.value.filter(t => t.id !== id)
}
</script>
<template>
<div class="todo-app">
<h1>Todo List</h1>
<div class="add-task">
<input
v-model="newTaskText"
@keyup.enter="addTask"
placeholder="Add new task"
>
<button @click="addTask">Add</button>
</div>
<ul class="task-list">
<li
v-for="task in tasks"
:key="task.id"
:class="{ completed: task.completed }"
>
<input
type="checkbox"
:checked="task.completed"
@change="toggleTask(task.id)"
>
<span>{{ task.text }}</span>
<button @click="removeTask(task.id)">Delete</button>
</li>
</ul>
<div class="stats">
<p>Completed: {{ completedCount }}</p>
<p>Remaining: {{ remainingCount }}</p>
</div>
</div>
</template>
<style scoped>
.todo-app {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.add-task {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.add-task input {
flex: 1;
padding: 8px;
font-size: 14px;
}
.task-list {
list-style: none;
padding: 0;
}
.task-list li {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border-bottom: 1px solid #eee;
}
.task-list li.completed span {
text-decoration: line-through;
opacity: 0.6;
}
.stats {
margin-top: 20px;
padding-top: 20px;
border-top: 2px solid #eee;
}
.stats p {
margin: 5px 0;
}
</style>
Template Syntax and Directives
Vue uses an HTML-based template syntax with special directives.
Text Interpolation:
<template>
<div>
<!-- Basic interpolation -->
<p>{{ message }}</p>
<!-- JavaScript expressions -->
<p>{{ count + 1 }}</p>
<p>{{ ok ? 'YES' : 'NO' }}</p>
<p>{{ message.split('').reverse().join('') }}</p>
<!-- Calling functions -->
<p>{{ formatDate(timestamp) }}</p>
</div>
</template>
v-bind - Attribute Binding:
<template>
<!-- Bind attribute -->
<img v-bind:src="imageUrl" v-bind:alt="imageAlt">
<!-- Shorthand -->
<img :src="imageUrl" :alt="imageAlt">
<!-- Dynamic attribute name -->
<button :[attributeName]="value">Click</button>
<!-- Bind multiple attributes -->
<div v-bind="objectOfAttrs"></div>
<!-- Class binding -->
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
<div :class="[activeClass, errorClass]"></div>
<div :class="[isActive ? activeClass : '', errorClass]"></div>
<!-- Style binding -->
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
<div :style="[baseStyles, overridingStyles]"></div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const imageUrl = ref('/path/to/image.jpg')
const imageAlt = ref('Description')
const isActive = ref(true)
const hasError = ref(false)
const activeClass = ref('active')
const errorClass = ref('text-danger')
const activeColor = ref('red')
const fontSize = ref(14)
const objectOfAttrs = reactive({
id: 'container',
class: 'wrapper'
})
</script>
v-on - Event Handling:
<template>
<!-- Method handler -->
<button v-on:click="handleClick">Click me</button>
<!-- Shorthand -->
<button @click="handleClick">Click me</button>
<!-- Inline handler -->
<button @click="count++">Increment</button>
<!-- Pass arguments -->
<button @click="handleClick('hello', $event)">Click</button>
<!-- Event modifiers -->
<form @submit.prevent="onSubmit">Submit</form>
<button @click.stop="handleClick">Stop Propagation</button>
<div @click.self="handleClick">Only Self</div>
<button @click.once="handleClick">Once</button>
<!-- Key modifiers -->
<input @keyup.enter="submit">
<input @keyup.esc="cancel">
<input @keyup.ctrl.s="save">
<!-- Mouse modifiers -->
<button @click.left="handleLeft">Left Click</button>
<button @click.right="handleRight">Right Click</button>
<button @click.middle="handleMiddle">Middle Click</button>
</template>
<script setup>
function handleClick(message, event) {
console.log(message, event)
}
function onSubmit() {
console.log('Form submitted')
}
</script>
v-model - Two-Way Binding:
<template>
<!-- Text input -->
<input v-model="text">
<p>{{ text }}</p>
<!-- Textarea -->
<textarea v-model="message"></textarea>
<!-- Checkbox -->
<input type="checkbox" v-model="checked">
<!-- Multiple checkboxes -->
<input type="checkbox" value="Vue" v-model="checkedNames">
<input type="checkbox" value="React" v-model="checkedNames">
<input type="checkbox" value="Angular" v-model="checkedNames">
<p>{{ checkedNames }}</p>
<!-- Radio -->
<input type="radio" value="One" v-model="picked">
<input type="radio" value="Two" v-model="picked">
<!-- Select -->
<select v-model="selected">
<option disabled value="">Please select</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<!-- Multiple select -->
<select v-model="multiSelected" multiple>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<!-- Modifiers -->
<input v-model.lazy="text"> <!-- Update on change, not input -->
<input v-model.number="age"> <!-- Auto typecast to number -->
<input v-model.trim="message"> <!-- Auto trim whitespace -->
<!-- Custom component v-model -->
<CustomInput v-model="searchText" />
</template>
<script setup>
import { ref } from 'vue'
const text = ref('')
const message = ref('')
const checked = ref(false)
const checkedNames = ref([])
const picked = ref('')
const selected = ref('')
const multiSelected = ref([])
const age = ref(0)
const searchText = ref('')
</script>
v-if, v-else-if, v-else - Conditional Rendering:
<template>
<div v-if="type === 'A'">
Type A
</div>
<div v-else-if="type === 'B'">
Type B
</div>
<div v-else-if="type === 'C'">
Type C
</div>
<div v-else>
Not A, B, or C
</div>
<!-- v-if with template (doesn't render wrapper) -->
<template v-if="ok">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</template>
</template>
<script setup>
import { ref } from 'vue'
const type = ref('A')
const ok = ref(true)
</script>
v-show - Toggle Display:
<template>
<!-- v-show toggles CSS display property -->
<h1 v-show="isVisible">Hello!</h1>
<!-- v-if vs v-show:
- v-if: truly conditional, destroys/recreates elements
- v-show: always rendered, toggles display CSS
- Use v-show for frequent toggles
- Use v-if for rarely changing conditions
-->
</template>
<script setup>
import { ref } from 'vue'
const isVisible = ref(true)
</script>
v-for - List Rendering:
<template>
<!-- Array iteration -->
<ul>
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
</ul>
<!-- With index -->
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ index }}: {{ item.text }}
</li>
</ul>
<!-- Object iteration -->
<ul>
<li v-for="(value, key) in user" :key="key">
{{ key }}: {{ value }}
</li>
</ul>
<!-- With index for objects -->
<ul>
<li v-for="(value, key, index) in user" :key="key">
{{ index }}. {{ key }}: {{ value }}
</li>
</ul>
<!-- Range -->
<span v-for="n in 10" :key="n">{{ n }}</span>
<!-- v-for with v-if (not recommended) -->
<!-- Use computed instead -->
<ul>
<li v-for="item in activeItems" :key="item.id">
{{ item.text }}
</li>
</ul>
<!-- v-for with template -->
<template v-for="item in items" :key="item.id">
<li>{{ item.text }}</li>
<li class="divider"></li>
</template>
</template>
<script setup>
import { ref, computed } from 'vue'
const items = ref([
{ id: 1, text: 'Learn Vue', active: true },
{ id: 2, text: 'Build app', active: false },
{ id: 3, text: 'Deploy', active: true }
])
const user = ref({
name: 'John',
age: 30,
email: 'john@example.com'
})
const activeItems = computed(() =>
items.value.filter(item => item.active)
)
</script>
Lifecycle Hooks
Lifecycle hooks let you run code at specific stages of a component's lifecycle.
Lifecycle Hooks in Composition API:
<script setup>
import {
ref,
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onErrorCaptured,
onActivated,
onDeactivated
} from 'vue'
const count = ref(0)
// Before component is mounted
onBeforeMount(() => {
console.log('Before mount')
})
// After component is mounted (DOM available)
onMounted(() => {
console.log('Mounted')
// Good for: API calls, DOM manipulation, timers
fetchData()
setupEventListeners()
})
// Before component updates (reactive data changed)
onBeforeUpdate(() => {
console.log('Before update')
})
// After component updates
onUpdated(() => {
console.log('Updated')
// Good for: DOM operations after data changes
})
// Before component is unmounted
onBeforeUnmount(() => {
console.log('Before unmount')
// Good for: Cleanup
})
// After component is unmounted
onUnmounted(() => {
console.log('Unmounted')
// Good for: Cleanup timers, event listeners
clearInterval(interval)
removeEventListeners()
})
// Error handling
onErrorCaptured((err, instance, info) => {
console.error('Error captured:', err, info)
return false // Prevent error from propagating
})
// For components in <keep-alive>
onActivated(() => {
console.log('Component activated')
})
onDeactivated(() => {
console.log('Component deactivated')
})
</script>
Lifecycle Diagram:
Creation Phase:
setup() → onBeforeMount() → onMounted()
Update Phase (when reactive data changes):
onBeforeUpdate() → onUpdated()
Destruction Phase:
onBeforeUnmount() → onUnmounted()
Component Communication
Props (Parent to Child)
<!-- Child Component: UserCard.vue -->
<script setup>
// Define props with types
const props = defineProps({
name: String,
age: Number,
email: String,
isActive: {
type: Boolean,
default: true
},
roles: {
type: Array,
default: () => []
},
profile: {
type: Object,
required: true,
validator: (value) => {
return value.id && value.name
}
}
})
// Props are reactive and can be used in computed
import { computed } from 'vue'
const displayName = computed(() =>
`${props.name} (${props.age})`
)
</script>
<template>
<div class="user-card">
<h3>{{ displayName }}</h3>
<p>{{ email }}</p>
<span v-if="isActive">Active</span>
<ul>
<li v-for="role in roles" :key="role">{{ role }}</li>
</ul>
</div>
</template>
<!-- Parent Component -->
<script setup>
import UserCard from './UserCard.vue'
import { reactive } from 'vue'
const user = reactive({
name: 'John Doe',
age: 30,
email: 'john@example.com',
isActive: true,
roles: ['admin', 'editor'],
profile: {
id: 1,
name: 'John'
}
})
</script>
<template>
<UserCard
:name="user.name"
:age="user.age"
:email="user.email"
:is-active="user.isActive"
:roles="user.roles"
:profile="user.profile"
/>
<!-- Or pass entire object -->
<UserCard v-bind="user" />
</template>
Emits (Child to Parent)
<!-- Child Component: TodoItem.vue -->
<script setup>
const props = defineProps({
todo: {
type: Object,
required: true
}
})
// Define emits
const emit = defineEmits(['toggle', 'delete', 'update'])
// Or with validation
const emit = defineEmits({
toggle: (id) => {
if (typeof id === 'number') {
return true
} else {
console.warn('Invalid toggle event payload')
return false
}
},
delete: (id) => typeof id === 'number',
update: (id, text) => {
return typeof id === 'number' && typeof text === 'string'
}
})
function handleToggle() {
emit('toggle', props.todo.id)
}
function handleDelete() {
emit('delete', props.todo.id)
}
function handleUpdate(newText) {
emit('update', props.todo.id, newText)
}
</script>
<template>
<div class="todo-item">
<input
type="checkbox"
:checked="todo.completed"
@change="handleToggle"
>
<span>{{ todo.text }}</span>
<button @click="handleDelete">Delete</button>
</div>
</template>
<!-- Parent Component -->
<script setup>
import TodoItem from './TodoItem.vue'
import { ref } from 'vue'
const todos = ref([
{ id: 1, text: 'Learn Vue', completed: false },
{ id: 2, text: 'Build app', completed: false }
])
function toggleTodo(id) {
const todo = todos.value.find(t => t.id === id)
if (todo) todo.completed = !todo.completed
}
function deleteTodo(id) {
todos.value = todos.value.filter(t => t.id !== id)
}
function updateTodo(id, text) {
const todo = todos.value.find(t => t.id === id)
if (todo) todo.text = text
}
</script>
<template>
<div>
<TodoItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
@toggle="toggleTodo"
@delete="deleteTodo"
@update="updateTodo"
/>
</div>
</template>
Provide/Inject (Ancestor to Descendant)
<!-- Ancestor Component: App.vue -->
<script setup>
import { provide, ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const theme = ref('dark')
const userSettings = ref({
fontSize: 14,
language: 'en'
})
// Provide to all descendants
provide('theme', theme)
provide('userSettings', userSettings)
// Provide with readonly to prevent modifications
import { readonly } from 'vue'
provide('theme', readonly(theme))
// Provide functions
function updateTheme(newTheme) {
theme.value = newTheme
}
provide('updateTheme', updateTheme)
</script>
<template>
<div>
<ChildComponent />
</div>
</template>
<!-- Descendant Component (any level deep) -->
<script setup>
import { inject } from 'vue'
// Inject provided values
const theme = inject('theme')
const userSettings = inject('userSettings')
const updateTheme = inject('updateTheme')
// With default value
const locale = inject('locale', 'en')
// With factory function for default
const settings = inject('settings', () => ({ mode: 'light' }))
</script>
<template>
<div :class="`theme-${theme}`">
<p>Font size: {{ userSettings.fontSize }}</p>
<button @click="updateTheme('light')">Light Theme</button>
<button @click="updateTheme('dark')">Dark Theme</button>
</div>
</template>
Slots (Parent Content Distribution)
<!-- Child Component: Card.vue -->
<script setup>
const props = defineProps({
title: String
})
</script>
<template>
<div class="card">
<!-- Named slot with fallback -->
<header>
<slot name="header">
<h2>{{ title }}</h2>
</slot>
</header>
<!-- Default slot -->
<main>
<slot>
<p>Default content</p>
</slot>
</main>
<!-- Named slot -->
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
<!-- Parent Component -->
<template>
<Card title="My Card">
<template #header>
<h1>Custom Header</h1>
</template>
<p>Main content goes here</p>
<template #footer>
<button>Action</button>
</template>
</Card>
</template>
<!-- Scoped Slots: Child exposes data to parent -->
<!-- Child Component: TodoList.vue -->
<script setup>
import { ref } from 'vue'
const todos = ref([
{ id: 1, text: 'Learn Vue', completed: false },
{ id: 2, text: 'Build app', completed: true }
])
</script>
<template>
<ul>
<li v-for="todo in todos" :key="todo.id">
<!-- Expose todo to parent via slot props -->
<slot :todo="todo" :index="todo.id"></slot>
</li>
</ul>
</template>
<!-- Parent Component -->
<template>
<TodoList>
<!-- Access slot props -->
<template #default="{ todo, index }">
<span :class="{ completed: todo.completed }">
{{ index }}. {{ todo.text }}
</span>
</template>
</TodoList>
<!-- Shorthand for default slot -->
<TodoList v-slot="{ todo }">
<span>{{ todo.text }}</span>
</TodoList>
</template>
State Management Patterns
Local Component State
<script setup>
import { ref, reactive } from 'vue'
// Simple counter state
const count = ref(0)
function increment() {
count.value++
}
// Form state
const formData = reactive({
name: '',
email: '',
message: ''
})
function submitForm() {
console.log('Submitting:', formData)
}
function resetForm() {
formData.name = ''
formData.email = ''
formData.message = ''
}
</script>
Composables (Reusable State Logic)
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
function decrement() {
count.value--
}
function reset() {
count.value = initialValue
}
return {
count,
doubled,
increment,
decrement,
reset
}
}
// Usage in component
<script setup>
import { useCounter } from '@/composables/useCounter'
const { count, doubled, increment, decrement, reset } = useCounter(10)
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<p>Doubled: {{ doubled }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="reset">Reset</button>
</div>
</template>
Mouse Position Composable:
// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
// Usage
<script setup>
import { useMouse } from '@/composables/useMouse'
const { x, y } = useMouse()
</script>
<template>
<p>Mouse position: {{ x }}, {{ y }}</p>
</template>
Fetch Composable:
// composables/useFetch.js
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
watchEffect(async () => {
loading.value = true
data.value = null
error.value = null
const urlValue = toValue(url)
try {
const response = await fetch(urlValue)
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
data.value = await response.json()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
})
return { data, error, loading }
}
// Usage
<script setup>
import { ref } from 'vue'
import { useFetch } from '@/composables/useFetch'
const userId = ref(1)
const url = computed(() => `https://api.example.com/users/${userId.value}`)
const { data: user, error, loading } = useFetch(url)
</script>
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<div v-else-if="user">{{ user.name }}</div>
</template>
Global State with Pinia
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// Option 1: Setup Stores (Composition API style)
export const useCounterStore = defineStore('counter', () => {
// State
const count = ref(0)
const name = ref('Counter')
// Getters
const doubleCount = computed(() => count.value * 2)
// Actions
function increment() {
count.value++
}
function decrement() {
count.value--
}
async function incrementAsync() {
await new Promise(resolve => setTimeout(resolve, 1000))
count.value++
}
return {
count,
name,
doubleCount,
increment,
decrement,
incrementAsync
}
})
// Option 2: Options Stores
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Counter'
}),
getters: {
doubleCount: (state) => state.count * 2,
doublePlusOne() {
return this.doubleCount + 1
}
},
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
async incrementAsync() {
await new Promise(resolve => setTimeout(resolve, 1000))
this.count++
}
}
})
// Usage in component
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
const counterStore = useCounterStore()
// Extract reactive state (preserves reactivity)
const { count, name, doubleCount } = storeToRefs(counterStore)
// Actions can be destructured directly
const { increment, decrement } = counterStore
</script>
<template>
<div>
<p>{{ name }}: {{ count }}</p>
<p>Double: {{ doubleCount }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</div>
</template>
Routing with Vue Router
Router Setup:
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'
const routes = [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
component: About
},
{
path: '/user/:id',
name: 'user',
component: () => import('@/views/User.vue'), // Lazy loading
props: true // Pass route params as props
},
{
path: '/posts',
name: 'posts',
component: () => import('@/views/Posts.vue'),
children: [
{
path: ':id',
name: 'post-detail',
component: () => import('@/views/PostDetail.vue')
}
]
},
{
path: '/:pathMatch(.*)*',
name: 'not-found',
component: () => import('@/views/NotFound.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// Navigation guards
router.beforeEach((to, from, next) => {
// Check authentication, etc.
if (to.meta.requiresAuth && !isAuthenticated()) {
next('/login')
} else {
next()
}
})
export default router
Using Router in Components:
<script setup>
import { useRouter, useRoute } from 'vue-router'
import { computed } from 'vue'
const router = useRouter()
const route = useRoute()
// Access route params
const userId = computed(() => route.params.id)
// Access query params
const searchQuery = computed(() => route.query.q)
// Programmatic navigation
function goToHome() {
router.push('/')
}
function goToUser(id) {
router.push({ name: 'user', params: { id } })
}
function goBack() {
router.back()
}
function goToSearch(query) {
router.push({ path: '/search', query: { q: query } })
}
</script>
<template>
<div>
<!-- Declarative navigation -->
<router-link to="/">Home</router-link>
<router-link :to="{ name: 'about' }">About</router-link>
<router-link :to="`/user/${userId}`">User Profile</router-link>
<!-- Active class styling -->
<router-link
to="/dashboard"
active-class="active"
exact-active-class="exact-active"
>
Dashboard
</router-link>
<!-- Programmatic navigation -->
<button @click="goToHome">Go Home</button>
<button @click="goToUser(123)">View User 123</button>
<button @click="goBack">Go Back</button>
<!-- Render matched component -->
<router-view />
<!-- Named views -->
<router-view name="sidebar" />
<router-view name="main" />
</div>
</template>
Advanced Features
Teleport
Move content to a different location in the DOM.
<script setup>
import { ref } from 'vue'
const showModal = ref(false)
</script>
<template>
<div class="app">
<h1>My App</h1>
<button @click="showModal = true">Open Modal</button>
<!-- Teleport modal to body -->
<Teleport to="body">
<div v-if="showModal" class="modal">
<div class="modal-content">
<h2>Modal Title</h2>
<p>Modal content</p>
<button @click="showModal = false">Close</button>
</div>
</div>
</Teleport>
</div>
</template>
<style>
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
}
</style>
Suspense
Handle async components with loading states.
<!-- Async component -->
<script setup>
const data = await fetch('/api/data').then(r => r.json())
</script>
<template>
<div>{{ data }}</div>
</template>
<!-- Parent using Suspense -->
<template>
<Suspense>
<!-- Component with async setup -->
<template #default>
<AsyncComponent />
</template>
<!-- Loading state -->
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
<!-- Error handling with Suspense -->
<script setup>
import { onErrorCaptured, ref } from 'vue'
const error = ref(null)
onErrorCaptured((err) => {
error.value = err
return true
})
</script>
<template>
<div v-if="error">Error: {{ error.message }}</div>
<Suspense v-else>
<AsyncComponent />
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
Transitions
Animate elements entering/leaving the DOM.
<script setup>
import { ref } from 'vue'
const show = ref(true)
</script>
<template>
<button @click="show = !show">Toggle</button>
<!-- Basic transition -->
<Transition>
<p v-if="show">Hello</p>
</Transition>
<!-- Named transition -->
<Transition name="fade">
<p v-if="show">Fade transition</p>
</Transition>
<!-- Custom classes -->
<Transition
enter-active-class="animate__animated animate__fadeIn"
leave-active-class="animate__animated animate__fadeOut"
>
<p v-if="show">Custom animation</p>
</Transition>
<!-- List transitions -->
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
</TransitionGroup>
</template>
<style>
/* Transition classes */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* List transitions */
.list-enter-active,
.list-leave-active {
transition: all 0.3s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
.list-move {
transition: transform 0.3s ease;
}
</style>
Custom Directives
Create custom directives for DOM manipulation.
// directives/focus.js
export const vFocus = {
mounted(el) {
el.focus()
}
}
// directives/click-outside.js
export const vClickOutside = {
mounted(el, binding) {
el.clickOutsideEvent = (event) => {
if (!(el === event.target || el.contains(event.target))) {
binding.value(event)
}
}
document.addEventListener('click', el.clickOutsideEvent)
},
unmounted(el) {
document.removeEventListener('click', el.clickOutsideEvent)
}
}
// Usage in component
<script setup>
import { vFocus } from '@/directives/focus'
import { vClickOutside } from '@/directives/click-outside'
import { ref } from 'vue'
const show = ref(false)
function closeDropdown() {
show.value = false
}
</script>
<template>
<!-- Auto-focus input -->
<input v-focus type="text">
<!-- Click outside to close -->
<div v-click-outside="closeDropdown">
<button @click="show = !show">Toggle</button>
<div v-if="show">Dropdown content</div>
</div>
</template>
Performance Optimization
Computed vs Methods
<script setup>
import { ref, computed } from 'vue'
const items = ref([1, 2, 3, 4, 5])
// Computed: cached, only re-runs when dependencies change
const total = computed(() => {
console.log('Computing total')
return items.value.reduce((sum, n) => sum + n, 0)
})
// Method: runs on every render
function getTotal() {
console.log('Getting total')
return items.value.reduce((sum, n) => sum + n, 0)
}
</script>
<template>
<!-- Computed is called once and cached -->
<p>{{ total }}</p>
<p>{{ total }}</p>
<!-- Method is called twice -->
<p>{{ getTotal() }}</p>
<p>{{ getTotal() }}</p>
</template>
v-once and v-memo
<template>
<!-- Render once, never update -->
<div v-once>
<h1>{{ title }}</h1>
<p>{{ description }}</p>
</div>
<!-- Memoize based on dependencies -->
<div v-memo="[count, message]">
<p>{{ count }}</p>
<p>{{ message }}</p>
<!-- Only re-renders when count or message changes -->
</div>
<!-- Useful for long lists -->
<div
v-for="item in list"
:key="item.id"
v-memo="[item.selected]"
>
<!-- Only re-renders when item.selected changes -->
{{ item.name }}
</div>
</template>
Lazy Loading Components
<script setup>
import { defineAsyncComponent } from 'vue'
// Lazy load component
const HeavyComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
)
// With loading and error components
const AsyncComponent = defineAsyncComponent({
loader: () => import('./AsyncComponent.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200,
timeout: 3000
})
</script>
<template>
<Suspense>
<HeavyComponent />
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
Virtual Scrolling
<script setup>
import { ref, computed } from 'vue'
const items = ref(Array.from({ length: 10000 }, (_, i) => ({
id: i,
text: `Item ${i}`
})))
const containerHeight = 400
const itemHeight = 50
const visibleCount = Math.ceil(containerHeight / itemHeight)
const scrollTop = ref(0)
const startIndex = computed(() =>
Math.floor(scrollTop.value / itemHeight)
)
const endIndex = computed(() =>
Math.min(startIndex.value + visibleCount + 1, items.value.length)
)
const visibleItems = computed(() =>
items.value.slice(startIndex.value, endIndex.value)
)
const offsetY = computed(() =>
startIndex.value * itemHeight
)
const totalHeight = computed(() =>
items.value.length * itemHeight
)
function handleScroll(event) {
scrollTop.value = event.target.scrollTop
}
</script>
<template>
<div
class="virtual-scroll-container"
:style="{ height: `${containerHeight}px`, overflow: 'auto' }"
@scroll="handleScroll"
>
<div :style="{ height: `${totalHeight}px`, position: 'relative' }">
<div :style="{ transform: `translateY(${offsetY}px)` }">
<div
v-for="item in visibleItems"
:key="item.id"
:style="{ height: `${itemHeight}px` }"
>
{{ item.text }}
</div>
</div>
</div>
</div>
</template>
TypeScript Integration
Basic Setup
// Component with TypeScript
<script setup lang="ts">
import { ref, computed, type Ref } from 'vue'
// Type annotations
const count: Ref<number> = ref(0)
const message = ref<string>('Hello')
// Interface for objects
interface User {
id: number
name: string
email: string
}
const user = ref<User>({
id: 1,
name: 'John',
email: 'john@example.com'
})
// Props with types
interface Props {
title: string
count?: number
user: User
}
const props = withDefaults(defineProps<Props>(), {
count: 0
})
// Emits with types
interface Emits {
(e: 'update', value: number): void
(e: 'delete', id: number): void
}
const emit = defineEmits<Emits>()
// Computed with type inference
const doubled = computed(() => props.count * 2)
// Typed function
function updateUser(id: number, name: string): void {
user.value.id = id
user.value.name = name
}
</script>
Composables with TypeScript
// composables/useCounter.ts
import { ref, computed, type Ref, type ComputedRef } from 'vue'
interface UseCounterReturn {
count: Ref<number>
doubled: ComputedRef<number>
increment: () => void
decrement: () => void
}
export function useCounter(initialValue = 0): UseCounterReturn {
const count = ref(initialValue)
const doubled = computed(() => count.value * 2)
function increment(): void {
count.value++
}
function decrement(): void {
count.value--
}
return {
count,
doubled,
increment,
decrement
}
}
Testing
Component Testing with Vitest
// Counter.test.js
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Counter from './Counter.vue'
describe('Counter', () => {
it('renders initial count', () => {
const wrapper = mount(Counter, {
props: {
initialCount: 5
}
})
expect(wrapper.text()).toContain('5')
})
it('increments count when button clicked', async () => {
const wrapper = mount(Counter)
await wrapper.find('button.increment').trigger('click')
expect(wrapper.vm.count).toBe(1)
expect(wrapper.text()).toContain('1')
})
it('emits update event', async () => {
const wrapper = mount(Counter)
await wrapper.find('button.increment').trigger('click')
expect(wrapper.emitted()).toHaveProperty('update')
expect(wrapper.emitted('update')[0]).toEqual([1])
})
})
Composable Testing
// useCounter.test.js
import { describe, it, expect } from 'vitest'
import { useCounter } from './useCounter'
describe('useCounter', () => {
it('initializes with default value', () => {
const { count } = useCounter()
expect(count.value).toBe(0)
})
it('initializes with custom value', () => {
const { count } = useCounter(10)
expect(count.value).toBe(10)
})
it('increments count', () => {
const { count, increment } = useCounter()
increment()
expect(count.value).toBe(1)
})
it('computes doubled value', () => {
const { count, doubled, increment } = useCounter()
expect(doubled.value).toBe(0)
increment()
expect(doubled.value).toBe(2)
})
})
Best Practices
1. Use Composition API for Complex Logic
Composition API provides better code organization and reusability.
<script setup>
// Good: Organized by feature
import { useUser } from '@/composables/useUser'
import { useProducts } from '@/composables/useProducts'
import { useCart } from '@/composables/useCart'
const { user, login, logout } = useUser()
const { products, fetchProducts } = useProducts()
const { cart, addToCart, removeFromCart } = useCart()
</script>
2. Keep Components Small and Focused
Break large components into smaller, reusable pieces.
<!-- Good: Focused components -->
<template>
<div>
<UserHeader :user="user" />
<UserProfile :user="user" />
<UserPosts :posts="posts" />
</div>
</template>
<!-- Bad: One large component -->
<template>
<div>
<!-- 500 lines of mixed concerns -->
</div>
</template>
3. Use Computed for Derived State
Don't compute values in templates or methods.
<script setup>
import { ref, computed } from 'vue'
const items = ref([...])
// Good: Computed property
const activeItems = computed(() =>
items.value.filter(item => item.active)
)
// Bad: Method called in template
function getActiveItems() {
return items.value.filter(item => item.active)
}
</script>
<template>
<!-- Good -->
<div v-for="item in activeItems" :key="item.id">
<!-- Bad: Computed on every render -->
<div v-for="item in getActiveItems()" :key="item.id">
</template>
4. Always Use Keys in v-for
Keys help Vue identify which items have changed.
<!-- Good -->
<div v-for="item in items" :key="item.id">
<!-- Bad: No key -->
<div v-for="item in items">
<!-- Bad: Using index as key (for dynamic lists) -->
<div v-for="(item, index) in items" :key="index">
5. Avoid v-if with v-for
Use computed properties to filter lists instead.
<script setup>
import { computed } from 'vue'
// Good: Filter with computed
const activeItems = computed(() =>
items.value.filter(item => item.active)
)
</script>
<template>
<!-- Good -->
<div v-for="item in activeItems" :key="item.id">
<!-- Bad: v-if with v-for -->
<div v-for="item in items" :key="item.id" v-if="item.active">
</template>
6. Prop Validation
Always validate props in production components.
<script setup>
// Good: Validated props
defineProps({
title: {
type: String,
required: true
},
count: {
type: Number,
default: 0,
validator: (value) => value >= 0
},
status: {
type: String,
validator: (value) => ['draft', 'published', 'archived'].includes(value)
}
})
</script>
7. Use Provide/Inject Sparingly
Provide/Inject is for deep component trees, not a replacement for props.
<!-- Good: Use for app-level state -->
<script setup>
provide('theme', theme)
provide('i18n', i18n)
</script>
<!-- Bad: Use for direct parent-child communication -->
<!-- Use props instead -->
8. Cleanup in onUnmounted
Always cleanup side effects to prevent memory leaks.
<script setup>
import { onMounted, onUnmounted } from 'vue'
let interval
onMounted(() => {
interval = setInterval(() => {
// Do something
}, 1000)
})
onUnmounted(() => {
clearInterval(interval)
})
</script>
9. Use Scoped Styles
Prevent style leaking with scoped styles.
<style scoped>
/* Styles only apply to this component */
.button {
background: blue;
}
</style>
<!-- Deep selector for child components -->
<style scoped>
.parent :deep(.child) {
color: red;
}
</style>
10. Lazy Load Routes
Improve initial load time with route-based code splitting.
const routes = [
{
path: '/',
component: () => import('@/views/Home.vue')
},
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue')
}
]
Summary
This Vue.js development skill covers:
- Reactivity System: ref(), reactive(), computed(), watch()
- Composition API: , composables, lifecycle hooks
- Single-File Components: Template, script, and style organization
- Directives: v-if, v-for, v-model, v-bind, v-on
- Component Communication: Props, emits, provide/inject, slots
- State Management: Local state, composables, Pinia
- Routing: Vue Router navigation and guards
- Advanced Features: Teleport, Suspense, Transitions, Custom Directives
- Performance: Computed vs methods, v-memo, lazy loading, virtual scrolling
- TypeScript: Type-safe props, emits, composables
- Testing: Component and composable testing
- Best Practices: Modern Vue 3 patterns and optimization techniques
The patterns and examples are based on official Vue.js documentation (Trust Score: 9.7) and represent modern Vue 3 development practices with Composition API and syntax.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
angular-development
Comprehensive Angular framework development covering components, directives, services, dependency injection, routing, and reactive programming based on official Angular documentation
svelte-development
Comprehensive Svelte development skill covering reactivity runes, components, stores, lifecycle, transitions, and modern Svelte 5 patterns
ui-design-patterns
Common interface patterns, navigation patterns, form patterns, data display patterns, feedback patterns, and accessibility considerations
react-patterns
Modern React development with hooks, component patterns, state management, and performance optimization for building scalable applications
mobile-design
Mobile UX patterns, touch interactions, gesture design, mobile-first principles, app navigation, and mobile performance
tailwind-css
Utility-first CSS framework for rapid UI development with responsive design, component patterns, and production optimization. Master core utilities, dark mode, customization, and modern component composition for building beautiful, performant user interfaces.
Didn't find tool you were looking for?