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():
javascript
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():
javascript
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():
javascript
// 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:
javascript
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:
javascript
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>:
vue
<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 <script setup> (verbose syntax):
vue
<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:
vue
<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:
vue
<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:
vue
<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:
vue
<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:
vue
<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'