How to use ref() in Vue 3
ref() is the foundational reactivity primitive in Vue 3’s Composition API, wrapping any value — string, number, boolean, object, or array — in a reactive container that Vue can track.
As the creator of CoreUI with Vue development experience since 2014, I use ref() as the default choice for reactive state in every Vue 3 component because it works consistently with all value types.
The one thing to remember is that ref values must be accessed with .value in JavaScript, while Vue templates automatically unwrap refs so you write {{ count }} not {{ count.value }}.
This distinction catches new Vue 3 developers by surprise but becomes second nature quickly.
Create reactive values with ref() and update them directly.
import { ref } from 'vue'
// Primitive values
const count = ref(0)
const name = ref('')
const isVisible = ref(false)
// Update by assigning to .value
count.value++
name.value = 'Alice'
isVisible.value = !isVisible.value
console.log(count.value) // 1
<template>
<!-- Templates auto-unwrap refs - no .value needed here -->
<p>Count: {{ count }}</p>
<p>Name: {{ name }}</p>
<button @click="count++">Increment</button>
</template>
Inside <template>, Vue unwraps refs automatically. Incrementing count directly in the template works because Vue translates it to count.value++ during compilation.
ref() for Objects and Arrays
ref() makes objects and arrays deeply reactive.
import { ref } from 'vue'
const user = ref({
name: 'Alice',
settings: {
theme: 'light',
notifications: true
}
})
const items = ref(['apple', 'banana', 'cherry'])
// Access and modify nested properties
user.value.name = 'Bob'
user.value.settings.theme = 'dark'
// Array mutations are tracked
items.value.push('date')
items.value.splice(0, 1) // remove 'apple'
console.log(user.value.settings.theme) // 'dark'
console.log(items.value) // ['banana', 'cherry', 'date']
ref() wraps the object in a Proxy, making all nested property changes reactive. This is equivalent to wrapping the value in reactive() but with the .value access pattern.
Template Refs for DOM Elements
Use ref() to get a reference to a DOM element or child component.
<template>
<input ref="inputEl" type="text" />
<button @click="focusInput">Focus Input</button>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const inputEl = ref(null)
onMounted(() => {
// inputEl.value is the actual DOM element after mount
inputEl.value?.focus()
})
function focusInput() {
inputEl.value?.focus()
}
</script>
When the ref attribute in the template matches a ref() variable name in <script setup>, Vue assigns the DOM element to inputEl.value after mounting. Before mounting, inputEl.value is null. The ?. optional chaining prevents errors before the component is mounted.
Watching ref() Values
React to changes in a ref with watch().
import { ref, watch } from 'vue'
const searchQuery = ref('')
watch(searchQuery, (newValue, oldValue) => {
console.log(`Search changed from "${oldValue}" to "${newValue}"`)
// Trigger search, debounce, etc.
})
Pass the ref directly (not searchQuery.value) to watch. Vue extracts the value automatically. Using .value would pass a plain value, not the reactive ref, and the watch would never trigger.
Best Practice Note
In CoreUI Vue components we prefer ref() over reactive() for most cases because it’s explicit — every reactive value has .value access, making it easy to identify reactive state in complex components. Use ref() for individual values and small groups; use reactive() for tightly related object properties like form state. For list rendering with array mutations, ref() with an array value tracks .push(), .pop(), .splice(), and other mutating methods automatically.



