Skip to main content
Vue.js for Beginners to Advanced
CHAPTER 05 Beginner

Vue Reactivity System

Updated: May 18, 2026
5 min read

# CHAPTER 5

Vue Reactivity System

1. Chapter Introduction

Vue's reactivity system is its most magical feature. When reactive data changes, Vue automatically updates every DOM node that depends on it — no manual DOM manipulation needed. Understanding how this works makes you a significantly better Vue developer. Vue 3 uses JavaScript Proxy under the hood (not Object.defineProperty like Vue 2).

Analogy: Imagine a spreadsheet. When you change cell A1, every formula that references A1 automatically recalculates. Vue's reactivity is the same — reactive data is A1, template expressions are the formulas.

2. Learning Objectives

  • Understand ref() for primitive values.
  • Understand reactive() for objects.
  • Know when to use ref() vs reactive().
  • Understand .value access pattern.
  • Build a live counter application.

3. The Reactivity Flow

text
123456789101112
Vue Reactivity Flow:

1. You declare: const count = ref(0)
2. Vue wraps count in a Proxy
3. Template reads count.value → Vue tracks this dependency
4. You write: count.value++
5. Vue detects the change via Proxy setter
6. Vue schedules a DOM update
7. Only the DOM nodes depending on count are updated
8. ← Back to step 3 (reactive cycle)

Key: Vue NEVER re-renders the whole page — only the affected nodes.

4. ref() — Reactive Primitives

vue
12345678910111213141516171819202122232425262728
<script setup>
import { ref } from &#039;vue'

// ref() wraps a primitive in a reactive container
const count = ref(0)           // number
const name = ref(&#039;Alice')      // string
const isLoggedIn = ref(false)  // boolean
const list = ref([1, 2, 3])    // array (also works with ref)
const user = ref({ id: 1 })   // object (also works with ref)

// In <script setup>: access with .value
console.log(count.value)       // 0
count.value++
console.log(count.value)       // 1

// In <template>: Vue auto-unwraps .value — no .value needed!
</script>

<template>
  <!-- NO .value in template — Vue unwraps automatically -->
  <p>Count: {{ count }}</p>
  <p>Name: {{ name }}</p>
  <button @click="count++">+</button>
  <button @click="count--">-</button>

  <!-- Working with ref arrays in template -->
  <li v-for="item in list" :key="item">{{ item }}</li>
</template>

5. reactive() — Reactive Objects

vue
123456789101112131415161718192021222324252627282930313233343536
<script setup>
import { reactive } from &#039;vue'

// reactive() wraps an object — all properties become reactive
const user = reactive({
  name: &#039;Alice',
  age: 28,
  address: {
    city: &#039;New York',
    country: &#039;USA'
  },
  hobbies: [&#039;coding', 'reading']
})

// Access WITHOUT .value (reactive returns the proxy directly)
console.log(user.name)    // 'Alice'
user.name = &#039;Bob'         // Reactive — triggers DOM update
user.age++                // Reactive
user.address.city = &#039;LA'  // Nested properties are also reactive!
user.hobbies.push(&#039;hiking') // Arrays work too

// ❌ IMPORTANT: Do NOT destructure reactive objects — loses reactivity!
// const { name } = user    // name becomes a plain string — NOT reactive

// ✅ Use toRefs() to destructure while keeping reactivity
import { toRefs } from &#039;vue'
const { name, age } = toRefs(user)
// name.value = 'Carol'   // Now reactive again
</script>

<template>
  <h2>{{ user.name }}</h2>
  <p>Age: {{ user.age }}</p>
  <p>City: {{ user.address.city }}</p>
  <button @click="user.age++">Birthday!</button>
</template>

6. ref() vs reactive() — When to Use Which

text
12345678910
ref():                          reactive():
─────────────────────────────   ─────────────────────────────
✅ Primitives (numbers, strings, bools)  ✅ Objects and complex state
✅ When you need to reassign whole value  ❌ Cannot reassign whole object
✅ Can be returned from composables  ✅ Groups related state together
❌ Requires .value in <script>  ❌ Cannot hold primitives directly
                                ❌ Loses reactivity when destructured

Modern recommendation: Use ref() for everything.
It&#039;s consistent — always .value in script, auto-unwrapped in template.

7. Advanced Reactivity Helpers

vue
12345678910111213141516171819202122
<script setup>
import { ref, reactive, computed, isRef, isReactive, toRef, toRefs, unref } from &#039;vue'

const count = ref(0)
const state = reactive({ count: 0 })

// Check if a value is reactive
console.log(isRef(count))       // true
console.log(isReactive(state))  // true

// Convert single reactive property to ref
const stateCount = toRef(state, &#039;count')
stateCount.value++    // Updates state.count too (same reference!)

// Convert reactive object properties to refs (for destructuring)
const { count: stateCount2 } = toRefs(state)

// Safely unwrap a ref OR plain value
const x = ref(42)
console.log(unref(x))   // 42
console.log(unref(42))  // 42 (works on plain values too)
</script>

8. Mini Project: Live Counter App

vue
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
<!-- LiveCounter.vue -->
<script setup>
import { ref, computed } from &#039;vue'

const count = ref(0)
const step = ref(1)
const history = ref([])

const isPositive = computed(() => count.value > 0)
const isNegative = computed(() => count.value < 0)
const isZero = computed(() => count.value === 0)

function increment() {
  count.value += step.value
  history.value.push({ action: `+${step.value}`, result: count.value })
}

function decrement() {
  count.value -= step.value
  history.value.push({ action: `-${step.value}`, result: count.value })
}

function reset() {
  history.value.push({ action: &#039;reset', result: 0 })
  count.value = 0
}

function clearHistory() {
  history.value = []
}
</script>

<template>
  <div class="counter-app">
    <h2>⚡ Live Counter</h2>

    <div class="display" :class="{ positive: isPositive, negative: isNegative, zero: isZero }">
      {{ count }}
    </div>

    <div class="step-control">
      <label>Step: </label>
      <input type="number" v-model.number="step" min="1" max="100" style="width: 60px; text-align: center;" />
    </div>

    <div class="buttons">
      <button class="btn-red" @click="decrement">− {{ step }}</button>
      <button class="btn-gray" @click="reset">Reset</button>
      <button class="btn-green" @click="increment">+ {{ step }}</button>
    </div>

    <div class="history">
      <div class="history-header">
        <h4>History ({{ history.length }})</h4>
        <button @click="clearHistory" class="btn-clear">Clear</button>
      </div>
      <div v-if="history.length === 0" class="empty">No history yet</div>
      <div v-for="(entry, i) in [...history].reverse()" :key="i" class="history-item">
        <span>{{ entry.action }}</span> → <strong>{{ entry.result }}</strong>
      </div>
    </div>
  </div>
</template>

<style scoped>
.counter-app { max-width: 340px; margin: 2rem auto; padding: 2rem; border-radius: 16px; background: #1e293b; color: white; font-family: sans-serif; }
.display { font-size: 5rem; text-align: center; font-weight: 800; padding: 1rem; border-radius: 12px; transition: .3s; }
.positive { color: #4ade80; }
.negative { color: #f87171; }
.zero { color: #94a3b8; }
.step-control { text-align: center; margin: .75rem 0; }
.step-control input { background: #334155; border: 1px solid #475569; color: white; padding: .25rem .5rem; border-radius: 6px; }
.buttons { display: flex; gap: .75rem; margin: 1rem 0; }
.buttons button { flex: 1; padding: .75rem; border: none; border-radius: 10px; font-size: 1rem; font-weight: 700; cursor: pointer; }
.btn-red { background: #ef4444; color: white; }
.btn-green { background: #22c55e; color: white; }
.btn-gray { background: #475569; color: white; }
.history { background: #0f172a; border-radius: 10px; padding: 1rem; max-height: 200px; overflow-y: auto; }
.history-header { display: flex; justify-content: space-between; align-items: center; }
h4 { margin: 0; font-size: .9rem; color: #94a3b8; }
.btn-clear { background: none; border: none; color: #f87171; cursor: pointer; font-size: .8rem; }
.history-item { padding: .35rem 0; border-bottom: 1px solid #1e293b; font-size: .85rem; }
.empty { color: #475569; font-size: .85rem; text-align: center; }
</style>

9. Common Mistakes

  • Accessing ref with .value in the template: {{ count.value }} in the template is WRONG. Vue auto-unwraps — just use {{ count }}.
  • Destructuring a reactive() object: const { name } = user breaks reactivity. Use toRefs(user) or switch to ref().

10. MCQs

Question 1

ref() is used for?

Question 2

How do you access a ref in <script setup>?

Question 3

How do you access a ref in <template>?

Question 4

What Vue 3 primitive does Vue use for reactivity?

Question 5

reactive() is best used for?

Question 6

What happens if you destructure a reactive() object directly?

Question 7

How do you safely destructure a reactive object?

Question 8

isRef(count) returns?

Question 9

What does unref(x) do?

Question 10

Vue 3 reactivity does NOT use?

11. Interview Questions

  • Q: Explain the difference between ref() and reactive() in Vue 3.
  • Q: Why does destructuring a reactive() object break reactivity?

12. Summary

Vue 3's reactivity system is powered by JavaScript Proxy — the most efficient and complete reactivity system in any frontend framework. ref() for primitives, reactive() for objects. In modern Vue, many developers use ref() for everything for consistency. The .value pattern in <script> and auto-unwrapping in <template> is the core pattern.

13. Next Chapter Recommendation

In Chapter 6: Vue Components, we build reusable Single File Components (SFCs) — the building blocks of every Vue application.

Finish this Chapter

Save your progress on your learning path and prepare for coding interview challenges.

Discussion

Join the discussion

Log in or create a free account to participate.

Sort: ·