---
title: "ref() or reactive()? How Vue tracks your data · vuejs.school"
description: "Vue updates the page when your data changes, but only if it can see the change. Here is how ref() and reactive() make that possible, and which to use."
canonical_url: "https://vuejs.school/community/blog/ref-or-reactive-how-vue-tracks-your-data"
meta:
  "og:description": "Vue updates the page when your data changes, but only if it can see the change. Here is how ref() and reactive() make that possible, and which to use."
  "og:image": "https://vuejs.school/_og/d/a_~cmVmKCkgb3IgcmVhY3RpdmUoKT8gSG93IFZ1ZSB0cmFja3MgeW91ciBkYXRhIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~cmVmKCkgb3IgcmVhY3RpdmUoKT8gSG93IFZ1ZSB0cmFja3MgeW91ciBkYXRh%2Cdescription_~VnVlIHVwZGF0ZXMgdGhlIHBhZ2Ugd2hlbiB5b3VyIGRhdGEgY2hhbmdlcywgYnV0IG9ubHkgaWYgaXQgY2FuIHNlZSB0aGUgY2hhbmdlLiBIZXJlIGlzIGhvdyByZWYoKSBhbmQgcmVhY3RpdmUoKSBtYWtlIHRoYXQgcG9zc2libGUsIGFuZCB3aGljaCB0byB1c2Uu%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9yZWYtb3ItcmVhY3RpdmUtaG93LXZ1ZS10cmFja3MteW91ci1kYXRhIg%2Cs_LULBAC44IXCwTEed.png"
  "og:image:alt": "ref() or reactive()? How Vue tracks your data · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "ref() or reactive()? How Vue tracks your data · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~cmVmKCkgb3IgcmVhY3RpdmUoKT8gSG93IFZ1ZSB0cmFja3MgeW91ciBkYXRhIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~cmVmKCkgb3IgcmVhY3RpdmUoKT8gSG93IFZ1ZSB0cmFja3MgeW91ciBkYXRh%2Cdescription_~VnVlIHVwZGF0ZXMgdGhlIHBhZ2Ugd2hlbiB5b3VyIGRhdGEgY2hhbmdlcywgYnV0IG9ubHkgaWYgaXQgY2FuIHNlZSB0aGUgY2hhbmdlLiBIZXJlIGlzIGhvdyByZWYoKSBhbmQgcmVhY3RpdmUoKSBtYWtlIHRoYXQgcG9zc2libGUsIGFuZCB3aGljaCB0byB1c2Uu%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9yZWYtb3ItcmVhY3RpdmUtaG93LXZ1ZS10cmFja3MteW91ci1kYXRhIg%2Cs_LULBAC44IXCwTEed.png"
  "twitter:image:alt": "ref() or reactive()? How Vue tracks your data · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## How Vue notices a change

Reactivity is a simple promise: when a value changes, everything that depends on it is worked out again. The page re-renders, a computed total is recalculated, a watcher runs. You do not call any of that yourself.

To keep that promise, Vue has to notice two things: when a value is read and when it is written. A plain variable gives it no way to notice either. So Vue wraps your data in objects it can watch, and works in two steps:

1. **Track.** While Vue renders a component or runs a `computed` or a watcher, each property it reads on one of these objects is recorded as a dependency of that work.
2. **Trigger.** When you write to that property, Vue tells everything that recorded it to update.

Both `ref()` and `reactive()` build those watchable objects.

## ref() for one value

`ref()` takes any value and returns a ref: a small object that holds your value in its `value` property.

```
import { ref } from 'vue'

const count = ref(0)
const name = ref('Asha')

count.value++
name.value = 'Asha K.'
```

The `.value` is not decoration. A ref is an object with a getter and a setter on `value`. When you read `count.value`, the getter records that something depends on `count`. When you write it, the setter triggers an update. A number on its own cannot do that, which is why the number has to live inside an object.

A ref can hold anything: a string, a boolean, an array, an object, `null`. When it holds an object, Vue makes that object deeply reactive too, so `user.value.name = 'Asha'` is tracked just like `user.value = newUser`.

## Leave out .value in the template

In a template, Vue removes the noise. A top-level ref (one declared directly in `<script setup>`) is unwrapped: Vue reads its `.value` for you. So you write `{{ count }}` and `@click="count++"`, not `count.value`:

*Counter.vue*

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

const count = ref(0)
</script>

<template>
  <button @click="count++">Clicked {{ count }} times</button>
</template>
```

The rule is: `.value` in script, no `.value` in the template.

Unwrapping has one limit. It applies only to top-level refs, not to refs nested inside a plain object. If `const box = { id: ref(1) }`, then `{{ box.id + 1 }}` renders `[object Object]1`, because `box.id` is still the ref object when the expression is evaluated. Keep refs at the top level of your script and this never comes up.

## reactive() for an object

`reactive()` takes an object and returns a reactive proxy of it. A proxy is a wrapper that looks exactly like the original object but intercepts every property read and write. There is no `.value`; you use the properties directly.

```
import { reactive } from 'vue'

const user = reactive({ name: 'Asha', visits: 1 })

user.visits++
user.name = 'Asha K.'
```

That reads nicely, and for a group of related fields, such as a form, it can be the clearer choice. But `reactive()` only works with object types: plain objects, arrays, and collections such as `Map` and `Set`. `reactive(0)` gives you back the plain `0`, because there is no object to wrap, and in development Vue warns `value cannot be made reactive: 0`.

One detail trips people up: the proxy is a different object from the one you pass in. Say you write `const original = { name: 'Asha', visits: 1 }` and then `const user = reactive(original)`. `user === original` is `false`, and changing `original.visits` directly does not tell Vue, because only the proxy reports reads and writes. Use what `reactive()` returned and leave the original alone.

## Destructuring reactive() loses reactivity

Destructuring means pulling properties out of an object into separate variables, like `const { visits } = user`. It is normal JavaScript and you will do it by reflex. Here it is in the `<script setup>` of `Profile.vue`, with a function that counts a visit:

*Profile.vue**Breaks***

```
import { reactive } from 'vue'

const user = reactive({ name: 'Asha', visits: 1 })
let { visits } = user

function recordVisit() {
  visits++
}
```

`visits` is now a plain number, a copy of the value at the moment you destructured. Vue cannot see reads or writes of a plain number. `recordVisit()` adds one to the copy, `user.visits` stays at 1, and the page does not change. There is no error and no warning, which is what makes this trap expensive.

The same thing happens when you pass `user.visits` into a function: the function receives a number, not a connection to `user`.

The fix is `toRefs()`, which turns each property of a reactive object into a ref that stays connected:

*Profile.vue**Works***

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

const user = reactive({ name: 'Asha', visits: 1 })
const { visits } = toRefs(user)

function recordVisit() {
  visits.value++
}
```

Now `visits` is a ref pointing at `user.visits`. Writing `visits.value++` updates `user`, and the page updates with it. Notice the `.value` is back. That is not a coincidence: the connection to Vue needs an object, and `.value` is the sign that you have one.

The simpler fix is to not use `reactive()` for this at all. Two refs, `name` and `visits`, cannot be destructured wrongly, because there is nothing to destructure.

## Why the docs recommend ref()

The Vue documentation lists [three limitations of `reactive()`](https://vuejs.org/guide/essentials/reactivity-fundamentals.html#limitations-of-reactive) and, because of them, recommends `ref()` as the primary API for declaring reactive state:

1. It only holds object types. Strings, numbers and booleans need a ref anyway, so a component that uses `reactive()` ends up mixing both styles.
2. You cannot replace the whole object. `state = reactive({ count: 1 })` points your variable at a new proxy, but anything that was tracking the old proxy keeps tracking it and never hears about the new one. With a ref, `state.value = { count: 1 }` is fine, because the ref itself is what is tracked.
3. It is not destructure-friendly, as you just saw.

A ref has none of these problems. The cost is `.value` in script, and after a week you stop seeing it. Our lessons use `ref()` for state unless there is a specific reason not to.

## shallowRef() for large data

Vue's reactivity is deep. When a ref holds an object, each nested object gets its own proxy the first time it is read, and every property read during a render is tracked.

For a form with ten fields that costs nothing. For a list of twenty thousand rows from an API, the tracking adds work to each render of the list, and you probably never change a single cell in place anyway. The Vue docs' [performance guide](https://vuejs.org/guide/best-practices/performance.html#reduce-reactivity-overhead-for-large-immutable-structures) says the overhead typically becomes noticeable when a single render reads 100,000 or more properties.

`shallowRef()` tracks only the `.value` assignment, not the contents. Here it holds rows loaded from an API (`/api/rows` stands in for your own endpoint):

```
import { shallowRef } from 'vue'

interface Row {
  id: number
  name: string
}

const rows = shallowRef<Row[]>([])

async function load() {
  const res = await fetch('/api/rows')
  rows.value = await res.json()
}
```

Replacing `rows.value` updates the page. Mutating `rows.value[0].name` does not, by design. Use it when the data is big and you treat it as a block that is swapped out, not edited.

**Check it stuck**3 questions from the Reactivity basics quiz.

[**Practice now**](https://vuejs.school/learn/beginner/reactivity-basics?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Tutorial · 5 min read**Forms and inputs: v-model modifiers, checkboxes and selects**](https://vuejs.school/community/blog/forms-and-inputs-v-model-modifiers-checkboxes-and-selects) [Tutorial · 6 min read**Computed properties: cache, don't recalculate**](https://vuejs.school/community/blog/computed-properties-cache-dont-recalculate)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/ref-or-reactive-how-vue-tracks-your-data#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-01-23","description":"Vue updates the page when your data changes, but only if it can see the change. Here is how ref() and reactive() make that possible, and which to use.","name":"ref() or reactive()? How Vue tracks your data","url":"https://vuejs.school/community/blog/ref-or-reactive-how-vue-tracks-your-data","breadcrumb":{"@id":"https://vuejs.school/community/blog/ref-or-reactive-how-vue-tracks-your-data#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/ref-or-reactive-how-vue-tracks-your-data"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/ref-or-reactive-how-vue-tracks-your-data#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-01-23","description":"Vue updates the page when your data changes, but only if it can see the change. Here is how ref() and reactive() make that possible, and which to use.","headline":"ref() or reactive()? How Vue tracks your data","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~cmVmKCkgb3IgcmVhY3RpdmUoKT8gSG93IFZ1ZSB0cmFja3MgeW91ciBkYXRhIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~cmVmKCkgb3IgcmVhY3RpdmUoKT8gSG93IFZ1ZSB0cmFja3MgeW91ciBkYXRh%2Cdescription_~VnVlIHVwZGF0ZXMgdGhlIHBhZ2Ugd2hlbiB5b3VyIGRhdGEgY2hhbmdlcywgYnV0IG9ubHkgaWYgaXQgY2FuIHNlZSB0aGUgY2hhbmdlLiBIZXJlIGlzIGhvdyByZWYoKSBhbmQgcmVhY3RpdmUoKSBtYWtlIHRoYXQgcG9zc2libGUsIGFuZCB3aGljaCB0byB1c2Uu%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9yZWYtb3ItcmVhY3RpdmUtaG93LXZ1ZS10cmFja3MteW91ci1kYXRhIg%2Cs_LULBAC44IXCwTEed.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/ref-or-reactive-how-vue-tracks-your-data#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/ref-or-reactive-how-vue-tracks-your-data#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/ref-or-reactive-how-vue-tracks-your-data#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"ref() or reactive()? How Vue tracks your data","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~cmVmKCkgb3IgcmVhY3RpdmUoKT8gSG93IFZ1ZSB0cmFja3MgeW91ciBkYXRhIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~cmVmKCkgb3IgcmVhY3RpdmUoKT8gSG93IFZ1ZSB0cmFja3MgeW91ciBkYXRh%2Cdescription_~VnVlIHVwZGF0ZXMgdGhlIHBhZ2Ugd2hlbiB5b3VyIGRhdGEgY2hhbmdlcywgYnV0IG9ubHkgaWYgaXQgY2FuIHNlZSB0aGUgY2hhbmdlLiBIZXJlIGlzIGhvdyByZWYoKSBhbmQgcmVhY3RpdmUoKSBtYWtlIHRoYXQgcG9zc2libGUsIGFuZCB3aGljaCB0byB1c2Uu%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9yZWYtb3ItcmVhY3RpdmUtaG93LXZ1ZS10cmFja3MteW91ci1kYXRhIg%2Cs_LULBAC44IXCwTEed.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~cmVmKCkgb3IgcmVhY3RpdmUoKT8gSG93IFZ1ZSB0cmFja3MgeW91ciBkYXRhIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~cmVmKCkgb3IgcmVhY3RpdmUoKT8gSG93IFZ1ZSB0cmFja3MgeW91ciBkYXRh%2Cdescription_~VnVlIHVwZGF0ZXMgdGhlIHBhZ2Ugd2hlbiB5b3VyIGRhdGEgY2hhbmdlcywgYnV0IG9ubHkgaWYgaXQgY2FuIHNlZSB0aGUgY2hhbmdlLiBIZXJlIGlzIGhvdyByZWYoKSBhbmQgcmVhY3RpdmUoKSBtYWtlIHRoYXQgcG9zc2libGUsIGFuZCB3aGljaCB0byB1c2Uu%2Ceyebrow_~VHV0b3JpYWwgwrcgNiBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9yZWYtb3ItcmVhY3RpdmUtaG93LXZ1ZS10cmFja3MteW91ci1kYXRhIg%2Cs_LULBAC44IXCwTEed.png"}]}
```