Reactivity: ref & reactive
Reactivity is the heart of Vue. When you store data in reactive state and the data changes, every part of the page that uses it updates by itself. Vue gives you two functions to create reactive state: ref() and reactive().
In this lesson you will learn how both work, why ref() needs .value, which mistakes silently break reactivity, and when the DOM actually updates.
ref(): the main way to create state
ref() takes any value (a number, string, boolean, array or object) and returns a ref object that wraps it. In JavaScript you read and change the value through its .value property. In the template, Vue unwraps refs for you, so you write just the name.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>ref</title>
<script src="https://cdn.jsdelivr.net/npm/vue@3.5.44/dist/vue.global.prod.js"></script>
<style>button { margin-right: 8px; }</style>
</head>
<body>
<div id="app">
<h2>Rohan's Fruit Cart</h2>
<p>Mangoes in basket: {{ mangoes }}</p>
<p>Bill: ₹{{ mangoes * pricePerMango }}</p>
<button @click="addMango">Add a mango</button>
<button @click="emptyBasket">Empty basket</button>
</div>
<script>
const { createApp, ref } = Vue;
createApp({
setup() {
const mangoes = ref(0);
const pricePerMango = 45;
function addMango() {
mangoes.value++; // in JavaScript: use .value
console.log("Now", mangoes.value, "mangoes");
}
function emptyBasket() {
mangoes.value = 0;
}
return { mangoes, pricePerMango, addMango, emptyBasket };
}
}).mount("#app");
</script>
</body>
</html>Notice pricePerMango is a plain number, not a ref. That is fine because it never changes. Only values that change and must update the page need to be reactive.
Why .value?
Plain JavaScript cannot tell when you read or change an ordinary variable such as let mangoes = 0. An object property is different: Vue can put a getter and setter on it. So ref() stores your value in a property called value. When the template reads mangoes.value, Vue records "this part of the page depends on mangoes". When you assign mangoes.value = 5, Vue knows exactly what to re-render. This recording of dependencies is called dependency tracking.
Deep reactivity
If you put an object or array inside a ref, Vue makes it deeply reactive: changes to nested properties and array methods like push() are detected too.
const order = ref({
customer: "Isha",
items: ["Paneer roll", "Lassi"]
});
order.value.items.push("Gulab jamun"); // detected, the page updates
order.value.customer = "Isha Verma"; // detected
order.value = { customer: "Kabir", items: [] }; // replacing the whole object also worksreactive(): a reactive object
reactive() makes an object itself reactive, so you do not need .value:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>reactive</title>
<script src="https://cdn.jsdelivr.net/npm/vue@3.5.44/dist/vue.global.prod.js"></script>
<style>button { margin-right: 8px; }</style>
</head>
<body>
<div id="app">
<p>{{ match.team }}: {{ match.runs }} runs in {{ match.overs }} overs</p>
<button @click="hitSix">Hit a six</button>
<button @click="brokenSix">Hit a six (broken copy)</button>
</div>
<script>
const { createApp, reactive } = Vue;
createApp({
setup() {
const match = reactive({ team: "Pune Panthers", runs: 87, overs: 10 });
function hitSix() {
match.runs += 6; // no .value needed
}
function brokenSix() {
let { runs } = match; // destructuring copies the number out
runs += 6; // changes only the local copy
console.log("Local copy is", runs, "but match.runs is", match.runs);
}
return { match, hitSix, brokenSix };
}
}).mount("#app");
</script>
</body>
</html>reactive() returns a Proxy: a JavaScript object that wraps the original and lets Vue see every read and write. The "broken copy" button shows its biggest weakness: when you destructure a property, you get a plain number with no connection to the object.
reactive() has three limitations:
- It works only with objects, arrays,
MapandSet.reactive(5)orreactive("Pune")will not work. - You cannot replace the whole object.
match = reactive({...})creates a new object, and the template still points to the old one. - Destructuring or passing a single property to a function loses reactivity, as you just saw.
Because of these limits, the Vue documentation recommends ref() as the primary way to declare state. Use reactive() when you have a group of related fields that you will always access through the object, such as a form.
Refs inside reactive objects
When a ref is a property of a reactive object, Vue unwraps it automatically:
const runs = ref(87);
const score = reactive({ runs });
console.log(score.runs); // 87, no .value needed
score.runs = 93;
console.log(runs.value); // 93, both point to the same refThis unwrapping does not happen for refs stored inside a reactive array or Map. There you still write list[0].value.
When does the page update?
Vue does not redraw the page the instant you change state. It collects all changes made in the same tick of JavaScript and applies them together in the next "update cycle". This avoids redrawing ten times when you change ten values. If you need to read the DOM after Vue has updated it, wait for nextTick():
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>nextTick</title>
<script src="https://cdn.jsdelivr.net/npm/vue@3.5.44/dist/vue.global.prod.js"></script>
</head>
<body>
<div id="app">
<p id="score">Wickets: {{ wickets }}</p>
<button @click="takeWicket">Take a wicket</button>
</div>
<script>
const { createApp, ref, nextTick } = Vue;
createApp({
setup() {
const wickets = ref(0);
async function takeWicket() {
wickets.value++;
const p = document.querySelector("#score");
console.log("Before nextTick:", p.textContent);
await nextTick();
console.log("After nextTick:", p.textContent);
}
return { wickets, takeWicket };
}
}).mount("#app");
</script>
</body>
</html>The first click prints:
Before nextTick: Wickets: 0 After nextTick: Wickets: 1
You rarely need nextTick() in everyday code. It is useful when you must measure an element or move focus right after a change.
Advanced: other reactivity helpers
shallowRef()tracks only.valuereplacement, not nested changes. Use it for large data that you always replace as a whole, such as a 10,000-row report, to save work.readonly(obj)returns a version that warns and refuses changes. Useful when you share state but want only one place to modify it.toRefs(obj)turns each property of a reactive object into a ref, so you can destructure without losing reactivity:const { runs } = toRefs(match).isRef(x)tells you whether something is a ref.
Common mistakes
- Forgetting
.valuein JavaScript.mangoes++on a ref does nothing useful. Writemangoes.value++. (In the template, do not write.value.) - Destructuring a reactive object.
const { runs } = matchcopies the value. Usematch.runs, ortoRefs(). - Reassigning a reactive variable. Change properties instead, or use a ref so you can replace
.value. - Making everything reactive. Constants and helper data that never change can stay plain variables.
Key points
ref(value)creates reactive state of any type; use.valuein JavaScript, but not in templates.- Objects and arrays inside a ref are deeply reactive, so nested changes and
push()are detected. reactive(obj)makes an object reactive through a Proxy, but cannot hold primitives, cannot be reassigned and loses reactivity when destructured.- The Vue docs recommend
ref()as the main way to declare state. - DOM updates are batched;
await nextTick()waits until the page has been updated.
Exercise
Create a "water tracker" for Meera. Use a ref for the number of glasses (start at 0) and a reactive object for her settings with name: "Meera" and goal: 8. Show "Meera has had N of 8 glasses today". Add buttons to add a glass and to raise the goal by 1. Log the new count to the console each time a glass is added.
Show solution
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Water Tracker</title>
<script src="https://cdn.jsdelivr.net/npm/vue@3.5.44/dist/vue.global.prod.js"></script>
</head>
<body>
<div id="app">
<p>{{ settings.name }} has had {{ glasses }} of {{ settings.goal }} glasses today</p>
<button @click="addGlass">Add a glass</button>
<button @click="settings.goal++">Raise goal</button>
</div>
<script>
const { createApp, ref, reactive } = Vue;
createApp({
setup() {
const glasses = ref(0);
const settings = reactive({ name: "Meera", goal: 8 });
function addGlass() {
glasses.value++;
console.log("Glasses:", glasses.value);
}
return { glasses, settings, addGlass };
}
}).mount("#app");
</script>
</body>
</html>The function uses glasses.value because it is JavaScript, while the template writes plain glasses. The reactive settings object is changed through its property, so it stays reactive.
Quick Quiz
1. Inside a setup() function, how do you add 1 to a ref called count?
In JavaScript a ref's value lives in .value; templates unwrap it for you.
2. What happens when you write const { runs } = reactive({ runs: 10 }) and then change runs?
Destructuring copies the primitive value out of the Proxy, so the connection is lost.
3. Which value can reactive() NOT make reactive?
reactive() only works with object types; use ref() for primitives.
4. What does await nextTick() wait for?
State changes are batched; nextTick() resolves after the DOM has been updated.
Learn Vue.js with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
