← Vue Menu · ← Prev: Vite & SFC
<script setup>
import { ref } from 'vue'
// ref() wraps any value in reactive container
const count = ref(0)
const name = ref('Mg Mg')
const loading = ref(false)
const items = ref([])
// Access/mutate with .value in script
count.value++
name.value = 'Ko Ko'
items.value.push({ id: 1, text: 'Buy milk' })
// In template: no .value needed (auto-unwrapped)
</script>
<template>
<p>Count: {{ count }}</p>
<button @click="count++">+1</button>
<input v-model="name">
</template>
<script setup>
import { reactive } from 'vue'
// reactive() makes the whole object reactive (no .value needed)
const form = reactive({
username: '',
email: '',
password: '',
})
const state = reactive({
products: [],
loading: false,
error: null,
page: 1,
})
// Mutate directly
function handleSubmit() {
form.username = 'trimmed' // reactive — template updates
state.loading = true
}
</script>
<template>
<input v-model="form.username">
<input v-model="form.email">
<p v-if="state.loading">Loading...</p>
</template>
| Feature | ref() | reactive() |
|---|---|---|
| Use for | Primitives (string, number, boolean) or arrays | Objects / complex state |
| .value | Required in script | Not needed |
| Destructure | Works (ref stays reactive) | Loses reactivity — use toRefs() |
| Template | Auto-unwrapped (no .value) | Access directly |
<script setup>
import { ref, computed } from 'vue'
const todos = ref([
{ id: 1, text: 'Learn Vue', done: true },
{ id: 2, text: 'Build app', done: false },
])
const search = ref('')
const showAll = ref(true)
// computed = cached, only re-runs when dependencies change
const filtered = computed(() => {
let list = showAll.value ? todos.value : todos.value.filter(t => !t.done)
return search.value
? list.filter(t => t.text.toLowerCase().includes(search.value.toLowerCase()))
: list
})
const doneCount = computed(() => todos.value.filter(t => t.done).length)
const totalCount = computed(() => todos.value.length)
const progress = computed(() => Math.round(doneCount.value / totalCount.value * 100))
// Writable computed (getter + setter)
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (v) => {
[firstName.value, lastName.value] = v.split(' ')
},
})
</script>
import { ref, watch, watchEffect } from 'vue'
const userId = ref(1)
const userData = ref(null)
// watch: explicit source + callback
watch(userId, async (newId, oldId) => {
console.log(`Changed from ${oldId} to ${newId}`)
userData.value = await fetchUser(newId)
})
// watch immediate: run on mount too
watch(userId, fetchUser, { immediate: true })
// watch multiple sources
watch([userId, role], ([id, r]) => {
console.log('User or role changed', id, r)
})
// watchEffect: auto-tracks dependencies
watchEffect(() => {
// runs whenever userId.value changes (auto-detected)
console.log('userId is now', userId.value)
})
// Deep watch (nested object changes)
const config = ref({ theme: 'dark', lang: 'mm' })
watch(config, (newVal) => {
localStorage.setItem('config', JSON.stringify(newVal))
}, { deep: true })
import { onMounted, onUnmounted, onBeforeUnmount, onUpdated } from 'vue'
// onMounted: after component is in DOM — fetch data here
onMounted(async () => {
const data = await fetch('/api/products').then(r => r.json())
products.value = data
})
// onUnmounted: cleanup (remove listeners, cancel timers)
let interval
onMounted(() => {
interval = setInterval(() => { clock.value = new Date() }, 1000)
})
onUnmounted(() => clearInterval(interval))
// Lifecycle order:
// setup() → onBeforeMount → onMounted
// (re-render) → onBeforeUpdate → onUpdated
// (unmount) → onBeforeUnmount → onUnmounted
// composables/useCounter.js
import { ref } from 'vue'
export function useCounter(initial = 0) {
const count = ref(initial)
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initial
return { count, increment, decrement, reset }
}
// composables/useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const stored = localStorage.getItem(key)
const data = ref(stored ? JSON.parse(stored) : defaultValue)
watch(data, (v) => localStorage.setItem(key, JSON.stringify(v)), { deep: true })
return data
}
// In any component:
<script setup>
import { useCounter } from '@/composables/useCounter'
import { useLocalStorage } from '@/composables/useLocalStorage'
const { count, increment } = useCounter(10)
const theme = useLocalStorage('theme', 'dark')
</script>
<!-- src/components/ProductCard.vue -->
<script setup>
const props = defineProps({
product: {
type: Object,
required: true,
},
showActions: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(['edit', 'delete', 'add-to-cart'])
function onEdit() { emit('edit', props.product) }
function onDelete() { emit('delete', props.product.id) }
function onCart() { emit('add-to-cart', props.product) }
</script>
<template>
<div class="card">
<h3>{{ product.name }}</h3>
<p>{{ product.price }}</p>
<div v-if="showActions">
<button @click="onEdit">Edit</button>
<button @click="onDelete">Delete</button>
<button @click="onCart">Add to Cart</button>
</div>
</div>
</template>
<!-- Parent usage -->
<ProductCard
:product="item"
:show-actions="isAdmin"
@edit="openEditModal"
@delete="removeProduct"
@add-to-cart="addItem"
/>
// Ancestor provides data
<script setup>
import { provide, ref } from 'vue'
const user = ref({ name: 'Mg Mg', role: 'admin' })
const theme = ref('dark')
provide('user', user) // any descendant can inject
provide('theme', theme)
provide('toggleTheme', () => {
theme.value = theme.value === 'dark' ? 'light' : 'dark'
})
</script>
// Deep descendant (doesn't need to pass via props chain)
<script setup>
import { inject } from 'vue'
const user = inject('user')
const theme = inject('theme')
const toggleTheme = inject('toggleTheme')
</script>