Reactivity is the heart of Vue. When you change your data, the UI automatically updates. The ref() function is the primary way to create reactive state in the Composition API.
What is ref()?
ref() takes a value and returns a reactive, mutable object with a single property .value that points to your data:
vue<script setup> import { ref } from 'vue' // Create reactive state const count = ref(0) const message = ref('Hello Vue!') const isActive = ref(true) const user = ref({ name: 'Alice', age: 25 }) // Access and modify with .value console.log(count.value) // 0 count.value++ console.log(count.value) // 1 message.value = 'Hello World!' isActive.value = false user.value.name = 'Bob' </script>
Why .value?
JavaScript primitives (numbers, strings, booleans) are passed by value, not reference. Vue can't track changes to plain variables:
javascript// This doesn't work - Vue can't track it let count = 0 count++ // Vue has no idea this happened // This works - Vue tracks the ref object const count = ref(0) count.value++ // Vue detects the change
The .value property lets Vue intercept access and track dependencies.
Automatic Unwrapping in Templates
Good news: you don't need .value in templates! Vue automatically unwraps refs:
vue<script setup> import { ref } from 'vue' const count = ref(0) const message = ref('Hello') </script> <template> <!-- No .value needed! --> <p>Count: {{ count }}</p> <p>Message: {{ message }}</p> <!-- Also works in attributes --> <input :value="message" /> <!-- And in expressions --> <p>Double: {{ count * 2 }}</p> </template>
Working with Different Types
Primitive Values
vue<script setup> import { ref } from 'vue' // Numbers const price = ref(29.99) const quantity = ref(1) // Strings const firstName = ref('John') const lastName = ref('Doe') // Booleans const isLoading = ref(false) const hasError = ref(false) // Null/Undefined const selectedItem = ref(null) const optionalValue = ref(undefined) </script>
Objects and Arrays
ref() works with objects and arrays too—they become deeply reactive:
vue<script setup> import { ref } from 'vue' const user = ref({ name: 'Alice', email: 'alice@example.com', settings: { theme: 'dark', notifications: true } }) const items = ref(['Apple', 'Banana', 'Cherry']) // All these trigger reactivity: user.value.name = 'Bob' user.value.settings.theme = 'light' items.value.push('Date') items.value[0] = 'Apricot' </script>
TypeScript with ref()
TypeScript infers the type automatically, or you can be explicit:
typescriptimport { ref } from 'vue' // Type inference const count = ref(0) // Ref<number> const message = ref('hello') // Ref<string> // Explicit typing const id = ref<number | null>(null) const items = ref<string[]>([]) type User = { id: number name: string email: string } const user = ref<User | null>(null) // Later: user.value = { id: 1, name: 'Alice', email: 'alice@example.com' }
Common Patterns
Toggle Boolean
vue<script setup> import { ref } from 'vue' const isOpen = ref(false) function toggle() { isOpen.value = !isOpen.value } </script> <template> <button @click="toggle"> {{ isOpen ? 'Close' : 'Open' }} </button> <div v-if="isOpen">Content here</div> </template>
Counter
vue<script setup> import { ref } from 'vue' const count = ref(0) function increment() { count.value++ } function decrement() { count.value-- } function reset() { count.value = 0 } </script> <template> <p>Count: {{ count }}</p> <button @click="decrement">-</button> <button @click="increment">+</button> <button @click="reset">Reset</button> </template>
Form Input
vue<script setup> import { ref } from 'vue' const searchQuery = ref('') const selectedOption = ref('all') function handleSearch() { console.log('Searching for:', searchQuery.value) } </script> <template> <input v-model="searchQuery" placeholder="Search..." /> <select v-model="selectedOption"> <option value="all">All</option> <option value="active">Active</option> <option value="completed">Completed</option> </select> </template>