<script setup>
// Composition API with <script setup>
import { ref } from 'vue'
// Props
const props = defineProps({
title: String,
count: {
type: Number,
default: 0
}
})
// Emits
const emit = defineEmits(['update', 'delete'])
// Reactive state
const message = ref('Hello!')
// Methods
function handleClick() {
emit('update', message.value)
}
</script>
<template>
<div>
<h1>{{ title }}</h1>
<p>{{ message }}</p>
<button @click="handleClick">Update</button>
</div>
</template>
<style scoped>
h1 {
color: #333;
}
</style>
<script setup>
import { ref, reactive, computed, watch, watchEffect } from 'vue'
// ref for primitives
const count = ref(0)
const name = ref('Vue')
// Access/modify ref value
console.log(count.value)
count.value++
// reactive for objects
const state = reactive({
user: { name: 'John', age: 30 },
items: []
})
// Direct mutation
state.user.name = 'Jane'
state.items.push({ id: 1 })
// Computed properties
const doubleCount = computed(() => count.value * 2)
// Writable computed
const fullName = computed({
get: () => `${state.user.name}`,
set: (value) => { state.user.name = value }
})
// Watch single ref
watch(count, (newVal, oldVal) => {
console.log(`Count changed: ${oldVal} -> ${newVal}`)
})
// Watch reactive object property
watch(
() => state.user.name,
(newName) => console.log('Name changed:', newName)
)
// Watch multiple sources
watch([count, name], ([newCount, newName]) => {
console.log('Values:', newCount, newName)
})
// Immediate watch
watch(count, (val) => console.log(val), { immediate: true })
// watchEffect - auto-tracks dependencies
watchEffect(() => {
console.log('Count is:', count.value)
})
</script>
<template>
<!-- Text interpolation -->
<p>{{ message }}</p>
<p>{{ user.name }}</p>
<p>{{ formatDate(date) }}</p>
<!-- Raw HTML -->
<div v-html="rawHtml"></div>
<!-- Attribute binding -->
<img :src="imageUrl" :alt="imageAlt">
<button :disabled="isDisabled">Click</button>
<!-- Dynamic attribute name -->
<a :[attributeName]="url">Link</a>
<!-- Class binding -->
<div :class="{ active: isActive, disabled: isDisabled }"></div>
<div :class="[baseClass, { active: isActive }]"></div>
<!-- Style binding -->
<div :style="{ color: textColor, fontSize: size + 'px' }"></div>
<div :style="[baseStyles, overrideStyles]"></div>
<!-- Event handling -->
<button @click="handleClick">Click</button>
<button @click="count++">Increment</button>
<input @input="onInput($event)">
<form @submit.prevent="onSubmit">
<!-- Event modifiers -->
<button @click.stop="onClick">Stop propagation</button>
<button @click.once="onClick">Only once</button>
<input @keyup.enter="submit">
<!-- Two-way binding -->
<input v-model="message">
<input v-model.trim="message">
<input v-model.number="age" type="number">
<input v-model.lazy="message">
</template>
<template>
<!-- v-if / v-else-if / v-else -->
<div v-if="type === 'A'">Type A</div>
<div v-else-if="type === 'B'">Type B</div>
<div v-else>Other type</div>
<!-- v-show (toggles display CSS) -->
<div v-show="isVisible">Always in DOM</div>
<!-- v-if on template (no wrapper element) -->
<template v-if="showDetails">
<h1>Title</h1>
<p>Description</p>
</template>
</template>
<template>
<!-- Array -->
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
<!-- With index -->
<li v-for="(item, index) in items" :key="item.id">
{{ index }}: {{ item.name }}
</li>
<!-- Object -->
<li v-for="(value, key, index) in object" :key="key">
{{ key }}: {{ value }}
</li>
<!-- Range -->
<span v-for="n in 10" :key="n">{{ n }}</span>
<!-- With v-if (use template wrapper) -->
<template v-for="item in items" :key="item.id">
<li v-if="item.isActive">{{ item.name }}</li>
</template>
</template>
<script setup>
// Props with types and defaults
const props = defineProps({
title: {
type: String,
required: true
},
count: {
type: Number,
default: 0
},
items: {
type: Array,
default: () => []
},
user: {
type: Object,
default: () => ({ name: 'Guest' })
},
callback: Function,
status: {
type: String,
validator: (value) => ['active', 'inactive'].includes(value)
}
})
// TypeScript props
interface Props {
title: string
count?: number
items?: string[]
}
const props = defineProps<Props>()
// With defaults (TypeScript)
const props = withDefaults(defineProps<Props>(), {
count: 0,
items: () => []
})
// Emits
const emit = defineEmits(['update', 'delete'])
const emit = defineEmits<{
(e: 'update', value: string): void
(e: 'delete', id: number): void
}>()
// Usage
emit('update', 'new value')
emit('delete', 123)
</script>
<script setup>
import {
onMounted,
onUpdated,
onUnmounted,
onBeforeMount,
onBeforeUpdate,
onBeforeUnmount
} from 'vue'
// Before mount
onBeforeMount(() => {
console.log('Before mount')
})
// After mount (DOM available)
onMounted(() => {
console.log('Mounted')
// Access DOM, start timers, fetch data
})
// Before update
onBeforeUpdate(() => {
console.log('Before update')
})
// After update
onUpdated(() => {
console.log('Updated')
})
// Before unmount
onBeforeUnmount(() => {
console.log('Before unmount')
})
// After unmount (cleanup)
onUnmounted(() => {
console.log('Unmounted')
// Clean up timers, subscriptions
})
</script>
<script setup>
import { ref, onMounted } from 'vue'
// DOM element ref
const inputRef = ref(null)
// Component ref
const childRef = ref(null)
onMounted(() => {
// Access DOM element
inputRef.value.focus()
// Access child component methods/properties
childRef.value.someMethod()
})
</script>
<template>
<input ref="inputRef">
<ChildComponent ref="childRef" />
</template>
// 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 }
}
// composables/useFetch.js
import { ref, watchEffect } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(true)
watchEffect(async () => {
loading.value = true
try {
const res = await fetch(url.value || url)
data.value = await res.json()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
})
return { data, error, loading }
}
// Usage in component
import { useMouse } from '@/composables/useMouse'
import { useFetch } from '@/composables/useFetch'
const { x, y } = useMouse()
const { data, loading } = useFetch('/api/users')
<!-- Parent component -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
const updateTheme = (newTheme) => {
theme.value = newTheme
}
// Provide to all descendants
provide('theme', theme)
provide('updateTheme', updateTheme)
// Or provide object
provide('themeContext', {
theme,
updateTheme
})
</script>
<!-- Child/Descendant component -->
<script setup>
import { inject } from 'vue'
// Inject with default value
const theme = inject('theme', 'light')
const updateTheme = inject('updateTheme')
// Or inject object
const { theme, updateTheme } = inject('themeContext')
</script>
<!-- Parent using child with slots -->
<template>
<Card>
<!-- Default slot -->
<p>Main content</p>
<!-- Named slots -->
<template #header>
<h1>Card Title</h1>
</template>
<template #footer>
<button>Action</button>
</template>
<!-- Scoped slot -->
<template #item="{ item, index }">
<li>{{ index }}: {{ item.name }}</li>
</template>
</Card>
</template>
<!-- Card.vue (child with slots) -->
<template>
<div class="card">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot> <!-- Default slot -->
</main>
<ul>
<slot
v-for="(item, index) in items"
name="item"
:item="item"
:index="index"
></slot>
</ul>
<footer>
<slot name="footer">Default footer</slot>
</footer>
</div>
</template>
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{ path: '/', component: () => import('@/views/Home.vue') },
{ path: '/about', component: () => import('@/views/About.vue') },
{
path: '/users/:id',
component: () => import('@/views/User.vue'),
props: true
},
{
path: '/admin',
component: () => import('@/views/Admin.vue'),
meta: { requiresAuth: true }
},
{ path: '/:pathMatch(.*)*', component: NotFound }
]
const router = createRouter({
history: createWebHistory(),
routes
})
// Navigation guard
router.beforeEach((to, from) => {
if (to.meta.requiresAuth && !isAuthenticated) {
return '/login'
}
})
export default router
<!-- Using router in component -->
<script setup>
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
// Get route params
const userId = route.params.id
const searchQuery = route.query.q
// Navigation
function goToUser(id) {
router.push(`/users/${id}`)
// Or with object
router.push({ name: 'user', params: { id } })
}
function goBack() {
router.back()
}
</script>
<template>
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink :to="{ name: 'about' }">About</RouterLink>
</nav>
<RouterView />
</template>
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// Composition API style
export const useCounterStore = defineStore('counter', () => {
// State
const count = ref(0)
// Getters
const doubleCount = computed(() => count.value * 2)
// Actions
function increment() {
count.value++
}
async function fetchCount() {
const res = await fetch('/api/count')
count.value = await res.json()
}
return { count, doubleCount, increment, fetchCount }
})
// Options API style
export const useUserStore = defineStore('user', {
state: () => ({
name: '',
isLoggedIn: false
}),
getters: {
greeting: (state) => `Hello, ${state.name}!`
},
actions: {
login(name) {
this.name = name
this.isLoggedIn = true
},
logout() {
this.$reset()
}
}
})
<!-- Using Pinia store -->
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
const store = useCounterStore()
// Destructure with reactivity preserved
const { count, doubleCount } = storeToRefs(store)
// Actions can be destructured directly
const { increment } = store
</script>
<template>
<p>Count: {{ count }}</p>
<p>Double: {{ doubleCount }}</p>
<button @click="increment">+</button>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
// Basic async component
const AsyncModal = defineAsyncComponent(() =>
import('./components/Modal.vue')
)
// With loading and error states
const AsyncComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200,
timeout: 3000
})
</script>
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
<template>
<!-- Render modal at body level -->
<Teleport to="body">
<div v-if="showModal" class="modal">
<h2>Modal Title</h2>
<button @click="showModal = false">Close</button>
</div>
</Teleport>
<!-- Teleport to specific element -->
<Teleport to="#modals">
<Notification />
</Teleport>
</template>