Project Initialization
Modern Vue development relies on the official scaffolding tool to bootstrap projects with optimal defaults. Running the initialization command triggers an interactive prompt that configures TypeScript, testing frameworks, router integration, and package managers.
npm create vue@latest
The generated project structure separates concerns clearly, placing application entry points, routing configuration, and state management in dedicated directories. This approach streamlines development workflows and enforces modular architecture from the start.
Reactivity Fundamentals
Vue 3 replaces the Options API with the Composision API, offering superior logic reuse and type inference. The framework provides two primary methods for creating reactive state:
ref()wraps primitive values or objects and requires.valuefor access and mutation within JavaScript.reactive()creates a deep reactive proxy for objects, allowing direct property access without.value.
<script setup lang="ts">
import { ref, reactive } from 'vue'
const itemCount = ref(0)
const increment = () => {
itemCount.value += 1
}
const uiConfig = reactive({
theme: 'dark',
sidebarOpen: true
})
</script>
<template>
<div>
<p>Total interactions: {{ itemCount }}</p>
<button @click="increment">Update Count</button>
</div>
</template>
Computed Properties and Watchers
Derived state should be handled through computed properties. They cache results based on dependencies and only re-evaluate when those dependencies change. Avoid mutating computed values directly; instead, use a setter if two-way binding is required.
<script setup lang="ts">
import { ref, computed } from 'vue'
const taskList = ref([1, 2, 3, 4, 5, 6, 7, 8, 9])
const filteredTasks = computed(() => {
return taskList.value.filter(id => id > 4)
})
</script>
Side effects and data synchronization are managed via watch(). It supports shallow tracking by default but can monitor nested structures using the deep option. The immediate flag triggers the callback during component setup.
<script setup lang="ts">
import { ref, watch } from 'vue'
const searchQuery = ref('')
// Shallow watch on a single ref
watch([searchQuery], (newVal, oldVal) => {
console.log('Search updated:', newVal, oldVal)
})
// Deep watch with immediate execution
const formState = ref({ user: { name: '', email: '' } })
watch(formState, (newVal) => {
console.log('Form state changed deeply', newVal)
}, { deep: true, immediate: true })
</script>
Lifecycle Hooks
In the Composition API, lifecycle hooks are imported and prefixed with on. The setup function execution replaces beforeCreate and created. Other hooks align closely with their Options API counterparts.
<script setup lang="ts">
import { onMounted, onBeforeUnmount } from 'vue'
onMounted(() => {
console.log('Component rendered and mounted to DOM')
})
onBeforeUnmount(() => {
console.log('Cleaning up timers and event listeners')
})
</script>
Component Communication and Macros
Parent-to-child data flow utilizes defineProps(), which compiles to TypeScript interfaces or runtime validation. Child-to-parent communication relies on defineEmits() to dispatch custom events.
Parent Component
<script setup>
import UserCard from './UserCard.vue'
const handleRoleUpdate = (role) => {
console.log('Received role:', role)
}
</script>
<template>
<UserCard
title="Administrator"
:is-active="true"
@role-changed="handleRoleUpdate"
/>
</template>
Child Component
<script setup>
const props = defineProps({
title: { type: String, required: true },
isActive: { type: Boolean, default: false }
})
const emit = defineEmits(['role-changed'])
const updateRole = () => {
emit('role-changed', 'editor')
}
</script>
Accessing DOM elements or child component instances requires template refs. Child components must explicitly expose internal methods using defineExpose().
<script setup>
import { ref, onMounted } from 'vue'
const inputRef = ref(null)
defineExpose({
focusInput: () => inputRef.value?.focus()
})
onMounted(() => {
console.log(inputRef.value)
})
</script>
<template>
<input ref="inputRef" type="text" placeholder="Type here" />
</template>
For cross-component state sharing without prop drilling, provide() and inject() establish a dependancy injection system.
Ancestor
<script setup>
import { provide } from 'vue'
provide('appLocale', 'en-US')
</script>
Descendant
<script setup>
import { inject } from 'vue'
const locale = inject('appLocale')
</script>
Vue 3 introduces several compiler macros:
defineOptions: Configures component metadata likenameorinheritAttrs.defineModel: Simplifies two-way binding withv-modelby automatically handling props and emits.defineProps,defineEmits,defineExpose: Type-safe, compile-time macros that remove the need for explicit imports.
<script setup>
import { ref } from 'vue'
const toggleState = defineModel<boolean>()
</script>
<template>
<button @click="toggleState = !toggleState">
Active: {{ toggleState }}
</button>
</template>
State Management with Pinia
Pinia serves as the official state management solution, offering a simpler API and superior TypeScript support compared to Vuex.
Application Entry
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { setupStore } from './store'
const app = createApp(App)
app.use(router)
app.use(setupStore())
app.mount('#app')
Store Configuration Create a centralized store registry. Persistence plugins can be attached to automatically sync state with local storage.
import { createPinia } from 'pinia'
import piniaPluginPersist from 'pinia-plugin-persistedstate'
import { useAppSettings } from './modules/settings'
export { useAppSettings }
const pinia = createPinia()
pinia.use(piniaPluginPersist)
export default pinia
Module Definition
Stores encapsulate state, getters, and actions. The persist option enables automatic serialization.
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useAppSettings = defineStore('settings', () => {
const fontSize = ref(14)
const isDarkMode = ref(false)
const toggleTheme = () => {
isDarkMode.value = !isDarkMode.value
}
return {
fontSize,
isDarkMode,
toggleTheme
}
}, {
persist: true
})
Component Integration
When destructuring store state, use storeToRefs() to preserve reactivity. Actions can be called directly.
<script setup>
import { storeToRefs } from 'pinia'
import { useAppSettings } from '@/store'
const settingsStore = useAppSettings()
const { isDarkMode } = storeToRefs(settingsStore)
const applyTheme = () => {
settingsStore.toggleTheme()
}
</script>
<template>
<div :class="{ 'dark-theme': isDarkMode }">
<button @click="applyTheme">Toggle Theme</button>
</div>
</template>
Defining type interfaces alongside stores ensures compile-time safety for state shapes and action payloads. Structuring stores by feature domain rather than global singletons improves scalability and maintainability in larger applications.