Skip to main content
Advertisement

12.2 Composition API Types — ref, reactive, computed, watch

ref<T>

<script setup lang="ts">
import { ref } from 'vue'

// Type inference
const count = ref(0) // Ref<number>
const name = ref('') // Ref<string>
const active = ref(false) // Ref<boolean>

// Explicit types
const user = ref<User | null>(null)
const items = ref<string[]>([])

// Access via .value
count.value++
name.value = 'Alice'
user.value = { id: '1', name: 'Alice', email: 'alice@example.com' }

// Auto-unwrapped in templates — no .value needed
</script>

<template>
<p>{{ count }}</p> <!-- .value not needed -->
<p>{{ name }}</p>
</template>

reactive<T>

<script setup lang="ts">
import { reactive } from 'vue'

interface FormState {
name: string
email: string
age: number
address: {
city: string
country: string
}
}

// reactive: makes objects deeply reactive
const form = reactive<FormState>({
name: '',
email: '',
age: 0,
address: {
city: '',
country: 'US',
},
})

// Direct access (no .value needed)
form.name = 'Alice'
form.address.city = 'New York'

// ❌ Replacing the reactive object itself loses reactivity
// form = { name: 'Bob', ... } // Error

// ✅ Update with Object.assign
Object.assign(form, { name: 'Bob', email: 'bob@example.com' })
</script>

Choosing Between ref and reactive

<script setup lang="ts">
import { ref, reactive } from 'vue'

// Prefer ref for:
const count = ref(0) // Primitive values
const user = ref<User | null>(null) // Nullable values
const items = ref<string[]>([]) // Arrays

// Prefer reactive for:
const form = reactive({ // Forms with multiple fields
name: '',
email: '',
})

const state = reactive({ // Grouping related state
isLoading: false,
error: null as Error | null,
data: [] as User[],
})
</script>

computed<T>

<script setup lang="ts">
import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// Read-only computed
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// fullName: ComputedRef<string>

// Explicit type (for complex cases)
const filteredItems = computed<ProcessedItem[]>(() => {
return rawItems.value
.filter(item => item.active)
.map(item => processItem(item))
})

// Writable computed (get/set)
const fullNameWritable = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(value: string) {
const [first, last] = value.split(' ')
firstName.value = first
lastName.value = last
},
})

// Usage
fullNameWritable.value = 'Jane Smith'
// firstName.value === 'Jane', lastName.value === 'Smith'
</script>

watch and watchEffect

<script setup lang="ts">
import { ref, watch, watchEffect } from 'vue'

const count = ref(0)
const user = ref<User | null>(null)

// Watch a single ref
watch(count, (newValue: number, oldValue: number) => {
console.log(`${oldValue} → ${newValue}`)
})

// With options
watch(user, (newUser, oldUser) => {
if (newUser) {
console.log(`User changed: ${newUser.name}`)
}
}, {
deep: true, // Deep watching
immediate: true, // Run immediately
})

// Watch multiple sources
const searchQuery = ref('')
const currentPage = ref(1)

watch([searchQuery, currentPage], ([query, page], [prevQuery, prevPage]) => {
fetchData(query, page)
})

// Watch with getter function
const activeCount = ref(0)
watch(
() => activeCount.value * 2, // getter
(newValue) => {
console.log('doubled:', newValue)
}
)

// watchEffect: automatically tracks dependencies
const stopWatch = watchEffect(() => {
// Runs whenever reactive data accessed in this block changes
console.log(`count: ${count.value}, user: ${user.value?.name}`)
})

// Stop watching
stopWatch()
</script>

Custom Composable Type Design

// composables/useCounter.ts
import { ref, computed, Ref, ComputedRef } from 'vue'

interface UseCounterOptions {
initial?: number
min?: number
max?: number
step?: number
}

interface UseCounterReturn {
count: Ref<number>
doubled: ComputedRef<number>
increment: () => void
decrement: () => void
reset: () => void
isAtMin: ComputedRef<boolean>
isAtMax: ComputedRef<boolean>
}

export function useCounter(options: UseCounterOptions = {}): UseCounterReturn {
const {
initial = 0,
min = -Infinity,
max = Infinity,
step = 1,
} = options

const count = ref(initial)
const doubled = computed(() => count.value * 2)
const isAtMin = computed(() => count.value <= min)
const isAtMax = computed(() => count.value >= max)

function increment() {
if (count.value + step <= max) {
count.value += step
}
}

function decrement() {
if (count.value - step >= min) {
count.value -= step
}
}

function reset() {
count.value = initial
}

return {
count,
doubled,
increment,
decrement,
reset,
isAtMin,
isAtMax,
}
}
// composables/useFetch.ts
import { ref, Ref } from 'vue'

interface UseFetchReturn<T> {
data: Ref<T | null>
error: Ref<Error | null>
isLoading: Ref<boolean>
execute: () => Promise<void>
}

export function useFetch<T>(url: string): UseFetchReturn<T> {
const data = ref<T | null>(null) as Ref<T | null>
const error = ref<Error | null>(null)
const isLoading = ref(false)

async function execute() {
isLoading.value = true
error.value = null

try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
data.value = await response.json() as T
} catch (e) {
error.value = e instanceof Error ? e : new Error(String(e))
} finally {
isLoading.value = false
}
}

execute()

return { data, error, isLoading, execute }
}

Pro Tips

1. Preserve reactivity with toRef / toRefs

import { reactive, toRef, toRefs } from 'vue'

const state = reactive({ count: 0, name: 'Alice' })

// toRef: convert a single property to Ref
const count = toRef(state, 'count')
// count.value === state.count (synchronized)

// toRefs: convert all properties to Refs (preserves reactivity during destructuring)
const { count: countRef, name: nameRef } = toRefs(state)
// countRef.value === state.count

2. shallowRef / shallowReactive

import { shallowRef, shallowReactive } from 'vue'

// Shallow reactivity — only top level is reactive (performance optimization)
const bigObject = shallowRef({ nested: { value: 1 } })
bigObject.value = { nested: { value: 2 } } // ✅ Reactive update
bigObject.value.nested.value = 3 // ❌ Not a reactive update
Advertisement