Reactivity basics
Make state that updates the page with ref() and reactive(), and learn when to write .value.
What you'll learn
When you finish, you can do these.- 1Build a click counter that updates the page with ref()
- 2Explain why the page redraws when a ref changes
- 3Keep related fields in one object with reactive()
- 4Spot and fix a missing .value or a copy that never updates
Picture a page with a counter: a number and an Add one button. If the number is an ordinary JavaScript variable, a click changes the variable but not the page, because nothing told Vue to draw the page again. Reactivity is how Vue finds out that a value changed. You keep the state, the values that change while someone uses the page, in a ref: a box that holds a value and tells Vue when it changes.
The Code tab holds that page as one component, the file App.vue: the counter, with a short message after the number, and a line that counts the visits of a user named Asha. By the end you can explain why each number goes up when its button is clicked, and spot the quiet mistakes that keep a number stuck. You need one idea from the lesson Your first component: a name declared at the top level of <script setup>, outside any function, can be printed in the template, the file's HTML, with double braces.
A ref holds a value Vue can follow
In App.vue, the counter's number lives in a ref. ref(), imported from vue, takes a starting value and returns a ref: a small object that keeps your value in a property called value. Here the count starts at 0, and a function called addOne adds one to it.
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
function addOne() {
count.value++
}
</script>
In <script setup> you read and write the number through count.value. In the template you write {{ count }}, because Vue unwraps a top-level ref for you and prints the number inside.
<template>
<p>Count: {{ count }}</p>
<button @click="addOne">Add one</button>
</template>
@click runs code on a click (the Events and v-model lesson later in this track covers it). Here it calls addOne, which writes count.value, and the paragraph changes from "Count: 0" to "Count: 1".
In the Code tab the paragraph also prints note, a second ref that holds the message "Keep clicking". Once the count passes 5, addOne changes it to "That is a lot of clicks".
Reading makes a link, writing follows it
To know what to redraw after a click, Vue keeps a list of links between each ref and the code that reads it. While the template renders, that is, while Vue draws it on the page, it reads count.value to print {{ count }}, and the ref notes that this template depends on it. That is called tracking. When a click writes count.value++, the ref follows its links and re-runs everything that read it. That is called triggering.
App.vue has one more piece of state, user: an object with a name and a visits number, made with reactive() (the section reactive() for objects explains it). Vue links a read of user.visits the same way.
A template that never read user.visits is not redrawn when user.visits changes, and one that did read it always is. You never list what a template depends on: reading is enough.
Vue also does not redraw after every write. Changes made during one click are collected, and the page updates once, after your function finishes.
reactive() for objects
Under the counter, App.vue shows "Visits by Asha: 1": a name and a number of visits that belong together, kept in one object, user, made with reactive().
reactive() returns a proxy: a stand-in that looks like your object and tells Vue about each read and write. You use its properties without .value: the function visit runs user.visits++.
<script setup lang="ts">
import { ref, reactive } from 'vue'
const user = reactive({ name: 'Asha', visits: 1 })
function visit() {
user.visits++
}
</script>
The template prints {{ user.visits }} with no .value either, so one click on the Visit again button turns "Visits by Asha: 1" into "Visits by Asha: 2".
reactive() has limits, and ref() has none of them:
- It only accepts objects and arrays, not a number or a string.
- It only works while you go through the proxy: copy a property out and the copy is an ordinary value.
- Replacing the whole object is not tracked, because Vue follows the object's properties, not the variable.
That is why most Vue code reaches for ref() first and keeps reactive() for a group of related fields, like user.
What goes wrong
The first mistake is keeping the count in a plain variable. The page shows 0 forever, and no warning appears, because JavaScript sees nothing wrong.
<script setup lang="ts">
let count = 0
function addOne() { count++ }
</script>
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
function addOne() { count.value++ }
</script>
The variable does go up. If something else redraws the page later, the new number appears all at once, which makes this bug confusing.
The second is a missing .value. Writing count++ on a const ref throws TypeError: Assignment to constant variable., and your editor underlines it.
The quiet versions are worse. In addOne, the test that changes note compares the count with 5. Without .value, JavaScript turns the ref object into NaN, its not-a-number value, so the test is always false and the page shows no error.
if (count > 5) note.value = 'That is a lot of clicks' // always false, no error
if (count.value > 5) note.value = 'That is a lot of clicks'
With the broken line, six clicks show "Count: 6. Keep clicking".
Text in backticks, where ${…} puts a value into the text, has the same slip: `Clicked ${count} times` gives "Clicked object Object times".
The third is destructuring a reactive() object: pulling a property out into a variable of its own. const { visits } = user copies the number 1 into a new variable.
<script setup lang="ts">
const { visits } = user
</script>
<template>
<p>Visits by {{ user.name }}: {{ visits }}</p>
</template>
<template>
<p>Visits by {{ user.name }}: {{ user.visits }}</p>
</template>
The copy stays at 1 however often user.visits changes, so the line keeps saying "Visits by Asha: 1". When you do need separate variables, the Reactivity in depth lesson in the Advanced track shows toRefs(), which keeps each one linked.
What to remember
- A plain variable can change without the page knowing. State that the page shows goes in
ref()orreactive(). - In script you read and write a ref through
.value. In the template a top-level ref is unwrapped for you. - While something renders, Vue links it to every ref it reads, and a write to that ref re-runs it. Updates from one click are applied together.
- A
reactive()object keeps related fields together, used without.value. Destructuring it copies its values out, and the copies never update. - A missing
.valuerarely throws:count > 5is always false and${count}prints "object Object".
Try it
Open the Code tab and change the starter code. Each task says what you should see.- Add a
reset()function that setscount.value = 0andnote.value = 'Keep clicking', and a Reset button that calls it. Click Add one six times, then Reset: the page shows "Count: 0. Keep clicking" again. - Add
let plain = 0, a line<p>Plain: {{ plain }}</p>and a button with@click="plain++". Click it three times: the line still says 0. Now click Add one: Plain jumps to 3, because the redraw that the ref caused read the variable again. - Under
user, addconst { visits } = userand change the template to{{ visits }}. Click Visit again: the number stays at 1. Change it back touser.visitsand it counts up again.