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 oldObject.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
reactiveobserves 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:- Proxy starts tracking (
track()). - On getter, the dependency is collected.
- On setter, the related effect is triggered (
trigger()). - The template render is recalculated.
ref is a single-value store, reactive is a proxy container, and toRefs is a bridge between them.When to Use Which?
| Scenario | Use | Why |
|---|---|---|
| Single numeric/text value | ref | Simple and performant |
| Objects / arrays | reactive | Proxy tracks each property |
| You need destructuring | toRefs | Preserves reactivity |
| Returning state from a composable | toRefs | Makes 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 betweenref, 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:
ref→ single-value boxreactive→ proxy wrappertoRefs→ destructuring safety net
Using them correctly maximizes both performance and readability.