Learn

Learn

Vue Reactivity — ref, reactive and computed

Build a real mental model of Vue's reactivity system: what reactivity means, wrapping a single value with ref(), reading it through .value in script versus auto-unwrapping in the template, tracking objects with reactive(), deriving values with computed(), and how the template re-renders the moment state changes.

XCODXLearn · Updated

Introduction

Reactivity is the idea that your view stays in sync with your data automatically. In Vue, reactivity means the framework watches the state a component reads and re-renders the parts of the template that depend on that state whenever it changes. You describe *what* the interface should show for a given state; Vue takes responsibility for keeping the screen matching the state as it moves. There is no manual DOM update, no setState call to remember, and no diff to write by hand.

The whole system is built on a small vocabulary. ref() wraps a single reactive value, reactive() wraps an object whose properties are tracked, and computed() derives a new value from existing reactive state. Every Vue component you write leans on these three functions, so learning what each one does — and, just as importantly, when to reach for which — is the fastest way to become productive with the framework's Composition API.

The reason Vue needs these wrappers at all is that a plain JavaScript number or object cannot announce when it changes. ref() and reactive() create a tracked container Vue can observe: when you read the value during a render Vue records the dependency, and when you write to it Vue knows exactly which renders to run again. Hold that one picture in your head — *read to subscribe, write to notify* — and the rest of the reactivity API slots neatly into place.

Lessons

1. What reactivity means in Vue

A component is a function of its state: give it the same data and it renders the same output. Reactivity is what makes that relationship *live* — change the data and the output updates on its own. Under the hood Vue tracks, for every render, which pieces of reactive state were read, and it stores those as dependencies. When one of them is written to, Vue schedules the dependent component to render again. You never call an update function yourself; declaring the dependency by reading the value *is* the subscription.

2. A single reactive value with ref()

The ref() function is how you make one standalone value reactive — a number, a string, a boolean, or even an object. You pass the initial value and get back a *ref object*: a small container that holds your value and lets Vue track reads and writes to it. This is the workhorse of the Composition API, and most component state starts life as a ref().

<script setup>
import { ref } from 'vue';

// a single reactive number, starting at zero
const count = ref(0);
</script>

3. Reading a ref: .value in script, auto-unwrap in the template

Because a ref is a container, in your <script setup> code you reach the underlying data through its .value property — count.value to read it and count.value++ to change it. This is the one piece of ceremony refs ask of you, and forgetting the .value in script is the single most common beginner mistake. In the template, though, Vue *auto-unwraps* a top-level ref for you, so you write {{ count }} rather than {{ count.value }}. The rule is simple: .value in script, bare name in template.

<script setup>
import { ref } from 'vue';
const count = ref(0);

function increment() {
  count.value++;   // .value is required in script
}
</script>

<template>
  <!-- no .value here: Vue unwraps the ref automatically -->
  <button @click="increment">Count: {{ count }}</button>
</template>

4. Reactive objects with reactive()

Where ref() wraps a single value, reactive() wraps a whole object and makes *every* property reactive without any .value at all. You read and write state.items directly, both in script and in the template, and Vue tracks each property independently. It is a natural fit for a group of related fields — a form model, a settings object, a cart — that you want to treat as one unit.

<script setup>
import { reactive } from 'vue';

// every property of this object is reactive
const cart = reactive({ items: 2, price: 19 });

function addItem() {
  cart.items++;   // no .value — access properties directly
}
</script>

5. Choosing between ref() and reactive()

Both create reactive state, so which do you use? A practical rule: reach for ref() by default — it handles any type, primitives included, and the explicit .value in script makes it clear you are touching reactive state. Use reactive() when you have a cluster of properties that genuinely belong together as one object. Note two limits of reactive(): it only works on objects (never a lone number or string), and you lose reactivity if you destructure it or reassign the whole variable. Because a ref() sidesteps both traps, many teams standardise on ref() and treat reactive() as the occasional convenience.

6. Derived values with computed()

Often a value is not stored but *calculated* from other state — a total from a quantity and a price, a full name from a first and last, a filtered list from a source and a query. computed() is built for exactly this. You give it a getter function that reads reactive state and returns a result; Vue runs it, caches the result, and re-runs it only when one of the values it read changes. A computed is itself a ref, so in script you read it through .value and in the template you write its bare name, just like any other ref.

<script setup>
import { reactive, computed } from 'vue';
const cart = reactive({ items: 2, price: 19 });

// re-runs only when cart.items or cart.price changes
const total = computed(() => cart.items * cart.price);
</script>

<template>
  <p>Total: {{ total }}</p>
</template>

7. How the template re-renders on change

Tie it together and the loop is: the template reads some reactive state and Vue records those reads as dependencies of the render; you change the state — increment a ref, mutate a reactive property — and Vue notices the write; it then re-runs the render, but only for components that actually depended on what changed, and only the affected DOM is patched. That targeting is why a Vue app stays fast: a change to one value does not redraw the page, it updates precisely the nodes that showed that value.

That is the complete model. ref() makes a single value reactive and is read through .value in script but auto-unwrapped in the template; reactive() makes an object's properties reactive with no .value; computed() derives a cached value from that state; and every change flows automatically back to the DOM. Everything else in Vue's reactivity — watch, watchEffect, toRefs — is a refinement resting on this same frame.

Practice

Predict first: when you click the second button below, which numbers on screen change — and does Vue re-run the total even though you never touched it directly? (It does: total is a computed, so mutating cart.items re-runs its getter automatically.) Now try it live — a ref counter and a reactive cart with a computed total in one component:

Change the code and watch the preview stay in sync. Add a price field, or turn the total into a formatted string — the reactive graph keeps the template matching the state no matter how you rearrange it.

Next steps

You have the model — now go deeper. Read the ref() reference for every detail of the single-value wrapper and the computed() reference for derived state, follow how to handle form input with v-model for the most common reactive task, or browse the runnable reactivity examples for copy-paste patterns. Coming from another framework? Compare the approach with React state. Every snippet here opens in the editor.

Frequently asked questions

What is reactivity in Vue?
Reactivity is Vue keeping the DOM in sync with your data automatically. Vue tracks which reactive state a component reads while rendering, and when that state changes it re-runs the render and patches only the affected DOM — you never update the DOM by hand.
What is the difference between ref() and reactive()?
ref() wraps a single value of any type and is accessed through .value in script (auto-unwrapped in the template). reactive() wraps an object and makes its properties reactive with no .value, but only works on objects and breaks if you destructure or reassign it. Prefer ref() by default.
Why do I need to write .value in Vue?
A ref is a container object, so in <script setup> you read and write the value it holds through its .value property. Templates auto-unwrap top-level refs, so there you use the bare name instead. The .value only appears in script code.
What is computed() for?
computed() derives a value from other reactive state. You give it a getter; Vue caches the result and only re-runs the getter when one of the values it read changes, which makes it both convenient and efficient for totals, filtered lists and other derived values.