vuejs-development

Vue.js Development Skill

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "vuejs-development" with this command: npx skills add manutej/luxor-claude-marketplace/manutej-luxor-claude-marketplace-vuejs-development

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>

&#x3C;div class="add-task">
  &#x3C;input
    v-model="newTaskText"
    @keyup.enter="addTask"
    placeholder="Add new task"
  >
  &#x3C;button @click="addTask">Add&#x3C;/button>
&#x3C;/div>

&#x3C;ul class="task-list">
  &#x3C;li
    v-for="task in tasks"
    :key="task.id"
    :class="{ completed: task.completed }"
  >
    &#x3C;input
      type="checkbox"
      :checked="task.completed"
      @change="toggleTask(task.id)"
    >
    &#x3C;span>{{ task.text }}&#x3C;/span>
    &#x3C;button @click="removeTask(task.id)">Delete&#x3C;/button>
  &#x3C;/li>
&#x3C;/ul>

&#x3C;div class="stats">
  &#x3C;p>Completed: {{ completedCount }}&#x3C;/p>
  &#x3C;p>Remaining: {{ remainingCount }}&#x3C;/p>
&#x3C;/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>

&#x3C;!-- JavaScript expressions -->
&#x3C;p>{{ count + 1 }}&#x3C;/p>
&#x3C;p>{{ ok ? 'YES' : 'NO' }}&#x3C;/p>
&#x3C;p>{{ message.split('').reverse().join('') }}&#x3C;/p>

&#x3C;!-- Calling functions -->
&#x3C;p>{{ formatDate(timestamp) }}&#x3C;/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>

&#x3C;!-- Default slot -->
&#x3C;main>
  &#x3C;slot>
    &#x3C;p>Default content&#x3C;/p>
  &#x3C;/slot>
&#x3C;/main>

&#x3C;!-- Named slot -->
&#x3C;footer>
  &#x3C;slot name="footer">&#x3C;/slot>
&#x3C;/footer>

</div> </template>

<!-- Parent Component --> <template> <Card title="My Card"> <template #header> <h1>Custom Header</h1> </template>

&#x3C;p>Main content goes here&#x3C;/p>

&#x3C;template #footer>
  &#x3C;button>Action&#x3C;/button>
&#x3C;/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>

&#x3C;!-- Active class styling -->
&#x3C;router-link
  to="/dashboard"
  active-class="active"
  exact-active-class="exact-active"
>
  Dashboard
&#x3C;/router-link>

&#x3C;!-- Programmatic navigation -->
&#x3C;button @click="goToHome">Go Home&#x3C;/button>
&#x3C;button @click="goToUser(123)">View User 123&#x3C;/button>
&#x3C;button @click="goBack">Go Back&#x3C;/button>

&#x3C;!-- Render matched component -->
&#x3C;router-view />

&#x3C;!-- Named views -->
&#x3C;router-view name="sidebar" />
&#x3C;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>

&#x3C;!-- Teleport modal to body -->
&#x3C;Teleport to="body">
  &#x3C;div v-if="showModal" class="modal">
    &#x3C;div class="modal-content">
      &#x3C;h2>Modal Title&#x3C;/h2>
      &#x3C;p>Modal content&#x3C;/p>
      &#x3C;button @click="showModal = false">Close&#x3C;/button>
    &#x3C;/div>
  &#x3C;/div>
&#x3C;/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>

&#x3C;!-- Loading state -->
&#x3C;template #fallback>
  &#x3C;div>Loading...&#x3C;/div>
&#x3C;/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"

&#x3C;p v-if="show">Custom animation&#x3C;/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]"

&#x3C;!-- 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"

&#x3C;div :style="{ height: `${totalHeight}px`, position: 'relative' }">
  &#x3C;div :style="{ transform: `translateY(${offsetY}px)` }">
    &#x3C;div
      v-for="item in visibleItems"
      :key="item.id"
      :style="{ height: `${itemHeight}px` }"
    >
      {{ item.text }}
    &#x3C;/div>
  &#x3C;/div>
&#x3C;/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>

  1. 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>

  1. 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>

  1. 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">

  1. 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>

  1. 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>

  1. 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 -->

  1. 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>

  1. 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>

  1. 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.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

golang-backend-development

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

fastapi-microservices-development

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

spring-boot-development

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

expressjs-development

No summary provided by upstream source.

Repository SourceNeeds Review