Reactive state: ref
State is component data that changes while the app runs: the number of tasks, the text in a field, whether a menu is open. For Vue to notice changes and re-render the page, state is declared as reactive. The main way to do it is the ref function.
A plain variable does not work
<script setup>
let count = 0
</script>
<template>
<button @click="count++">Clicked: {{ count }}</button>
</template>The button always shows "Clicked: 0". The number in the variable grows, but Vue does not know about it and does not update the page.
The ref function
<!-- src/App.vue -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">Clicked: {{ count }}</button>
</template>Now every click increases the number on the button. ref(0) creates a wrapper object with the initial value 0. Vue tracks this object and re-renders everything that uses it.
The .value property
In the script the value lives in the .value property. In the template Vue unwraps it itself, so you do not write .value there:
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
console.log(count.value)
}
</script>
<template>
<button @click="increment">Clicked: {{ count }}</button>
</template>After each click the number on the button grows, and the browser console (F12, Console tab) prints the new value: 1, 2, 3.
| Where | Read | Write |
|---|---|---|
script setup | count.value | count.value = 5 |
template | count | count = 5 in a handler |
Any data type
ref accepts strings, numbers, arrays and objects. For a task list:
<script setup>
import { ref } from 'vue'
const todos = ref(['Buy bread', 'Call Anna'])
function addTodo() {
todos.value.push('New task')
}
</script>
<template>
<p>Tasks: {{ todos.length }}</p>
<button @click="addTodo">Add</button>
</template>Every click adds a string to the array, and the "Tasks" counter goes up. Vue notices even changes inside the array — push, splice, assigning an element.
Why const
A ref variable is declared with const: the wrapper itself never changes, only its .value does. This protects against replacing the whole object by accident.
Common mistake
Forgetting .value in the script:
<script setup>
import { ref } from 'vue'
const count = ref(0)
function reset() {
count = 0 // Error: cannot assign to a constant
}
</script>The right way is count.value = 0. If a comparison like count > 5 in the script never fires, you are probably comparing the wrapper, not the number.