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 // {{ count }}
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>
{{ message }} Count: {{ localCount }} Doubled: {{ doubledCount }}
{{ title }}
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>
Completed: {{ completedCount }} Remaining: {{ remainingCount }}
Todo List
Template Syntax and Directives
Vue uses an HTML-based template syntax with special directives.
Text Interpolation:
{{ message }} {{ count + 1 }} {{ ok ? 'YES' : 'NO' }} {{ message.split('').reverse().join('') }} {{ formatDate(timestamp) }}
v-bind - Attribute Binding:
v-on - Event Handling:
<script setup> function handleClick(message, event) { console.log(message, event) } function onSubmit() { console.log('Form submitted') } </script>
v-model - Two-Way Binding:
{{ text }}
{{ checkedNames }}
v-if, v-else-if, v-else - Conditional Rendering:
Paragraph 1 Paragraph 2Title
v-show - Toggle Display:
Hello!
<script setup> import { ref } from 'vue' const isVisible = ref(true) </script>v-for - List Rendering:
- {{ item.text }}
- {{ index }}: {{ item.text }}
- {{ key }}: {{ value }}
- {{ index }}. {{ key }}: {{ value }}
{{ n }}
- {{ item.text }}
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 inLifecycle Diagram:
Creation Phase: setup() → onBeforeMount() → onMounted()
Update Phase (when reactive data changes): onBeforeUpdate() → onUpdated()
Destruction Phase: onBeforeUnmount() → onUnmounted()
Component Communication Props (Parent to Child)
<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>
{{ email }}
{{ displayName }}
Emits (Child to Parent)
<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>
Provide/Inject (Ancestor to Descendant)
<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>
Font size: {{ userSettings.fontSize }}
Slots (Parent Content Distribution)
<script setup> const props = defineProps({ title: String }) </script>
Default content
{{ title }}
Custom Header
<p>Main content goes here</p>
<template #footer>
<button>Action</button>
</template>
<script setup> import { ref } from 'vue' const todos = ref([ { id: 1, text: 'Learn Vue', completed: false }, { id: 2, text: 'Build app', completed: true } ]) </script>
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>
Count: {{ count }} Doubled: {{ doubled }}
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>
Mouse position: {{ x }}, {{ y }}
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>
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>
{{ name }}: {{ count }} Double: {{ doubleCount }}
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>
Advanced Features Teleport
Move content to a different location in the DOM.
<script setup> import { ref } from 'vue' const showModal = ref(false) </script>
My App
Suspense
Handle async components with loading states.
<script setup> const data = await fetch('/api/data').then(r => r.json()) </script>
<!-- Loading state -->
<template #fallback>
<div>Loading...</div>
</template>
<script setup> import { onErrorCaptured, ref } from 'vue' const error = ref(null) onErrorCaptured((err) => { error.value = err return true }) </script>
Transitions
Animate elements entering/leaving the DOM.
<script setup> import { ref } from 'vue' const show = ref(true) </script>
Hello
Fade transition
<Transition enter-active-class="animate__animated animate__fadeIn" leave-active-class="animate__animated animate__fadeOut"
<p v-if="show">Custom animation</p>
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>
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>
{{ total }}
{{ total }}
{{ getTotal() }}
{{ getTotal() }}
v-once and v-memo
{{ title }}
{{ description }}
{{ count }}
{{ message }}
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>
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>
TypeScript Integration Basic Setup // Component with TypeScript
<script setup lang="ts"> import { ref, computed, type Ref } from 'vue' // Type annotations const count: RefComposables with TypeScript // composables/useCounter.ts import { ref, computed, type Ref, type ComputedRef } from 'vue'
interface UseCounterReturn {
count: Ref
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>- Keep Components Small and Focused
Break large components into smaller, reusable pieces.
- 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>