Cansu Arı Logo
Blog
What is it?

The Difference Between ref, reactive, and toRefs: Understanding True Reactivity

Vue’s reactivity system is Proxy-based. Understanding the difference between ref, reactive, and toRefs is the key to why we use .value.

  • #Vue.js

Skip to content

Let’s Understand Reactivity

In Vue, values on the screen update themselves without you even noticing. But without grasping how the system works, you’ll inevitably ask questions like “why do we use .value?” or “why wasn’t a change detected inside reactive?”

In this post we explore the differences between ref, reactive, and toRefs with real examples, along with the internal logic of Vue’s Proxy-based reactivity engine.

The Heart of Vue 3: Proxy-Based Reactivity

Vue 3 moved away from the old Object.defineProperty approach and uses the Proxy API. This allows Vue to observe all property accesses on an object and re-render dependent components only when necessary.

Vue effectively works like this:

const state = reactive({ count: 0 })
state.count++ // the Proxy says “hey, something changed!”

Behind the scenes, an observer called effect() collects dependencies and triggers the relevant render when changes occur.

What Is ref?

ref makes primitive values reactive. Types like number, string, and boolean aren’t directly observable in Vue, so they’re wrapped in a “box.”
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0
count.value++
Here, .value is your access gate to the Proxy-wrapped value. In templates, .value is unwrapped automatically, so this is enough:
<p>{{ count }}</p>
But on the JS side you must use .value.

💡 Tip: ref is always for a single value. If you’ll hold an object, prefer reactive.

What Is reactive?

reactive covers objects and arrays. The Proxy tracks every property change:
const user = reactive({
name: 'Cansu',
age: 28
})
user.age++ // reactive proxy kicks in
Note: if you place refs inside a reactive object, Vue will unwrap them. So you don’t write user.age.value; Vue opens .value for you.

Limitations of reactive

  • reactive observes the object via Proxy, but when you destructure, you can lose reactivity.
const state = reactive({ x: 1, y: 2 })
const { x } = state
x++ // no longer reactive!
That’s where toRefs() comes in 👇

toRefs() — Destructure Without Losing Reactivity

toRefs converts a reactive object’s properties into refs, preserving reactivity when destructuring.
import { reactive, toRefs } from 'vue'
const state = reactive({ x: 1, y: 2 })
const { x, y } = toRefs(state)
x.value++ // updates both x and state.x!
This keeps reactivity intact when spreading into components or passing state to Composition API functions.

Going Deeper: The Reactivity Flow

Vue follows these steps:
  1. Proxy starts tracking (track()).
  2. On getter, the dependency is collected.
  3. On setter, the related effect is triggered (trigger()).
  4. The template render is recalculated.
So ref is a single-value store, reactive is a proxy container, and toRefs is a bridge between them.

When to Use Which?

ScenarioUseWhy
Single numeric/text valuerefSimple and performant
Objects / arraysreactiveProxy tracks each property
You need destructuringtoRefsPreserves reactivity
Returning state from a composabletoRefsMakes a reactive object component-friendly

Bonus: shallowRef, shallowReactive

For performance, Vue offers “shallow” variants that only watch the first level and don’t proxy deep objects. Useful for big lists to avoid FPS drops.
import { shallowReactive } from 'vue'
const table = shallowReactive({ rows: [] }) // only the rows property is tracked

Conclusion

Understanding the differences between ref, reactive, and toRefs answers “why didn’t a change reflect on the screen?” in Vue. Vue’s Proxy-based system is an orchestra—each change updates only the affected component.

In short:

  • refsingle-value box
  • reactiveproxy wrapper
  • toRefsdestructuring safety net

Using them correctly maximizes both performance and readability.

All tags
  • Vue.js