DEV Community

Cover image for The Magic (and Gotchas) of Vue.js Reactivity: How It Knows Exactly What to Update
Bruno Xavier
Bruno Xavier

Posted on

The Magic (and Gotchas) of Vue.js Reactivity: How It Knows Exactly What to Update

The Magic (and Gotchas) of Vue.js Reactivity: How It Knows Exactly What to Update

If you've used Vue.js, you've probably experienced that "wow" moment: you change a variable and the UI updates automatically. No setState, no massive useEffect chains, no manual subscriptions. It feels like magic.

But it's not magic — it's a beautifully engineered reactivity system. And behind that simplicity lies one of the most interesting technical implementations in modern frontend frameworks.

What Makes Vue's Reactivity So Special?

Unlike React, which relies on Virtual DOM diffing and explicit hooks, Vue 3 uses native JavaScript Proxies (ES6) to create automatic dependency tracking.
Here's the trick in simple terms:

  1. When you read a reactive property (e.g. count.value), Vue secretly registers that the current effect (component render, watcher, or computed) depends on it.

  2. When you change that property, Vue only notifies the effects that actually depend on it.

This is called fine-grained reactivity — surgical updates without re-rendering entire components unnecessarily.

Practical Example with ref() and reactive()

vue<script setup>
import { ref, reactive, watchEffect } from 'vue'

// ref() is used for primitives or when you want .value
const count = ref(0)

// reactive() turns the entire object into a Proxy
const state = reactive({
  user: {
    name: 'Bruno',
    level: 42
  }
})

// watchEffect runs immediately and tracks dependencies automatically
watchEffect(() => {
  console.log(`User ${state.user.name} is at level ${state.user.level}`)
  console.log(`Counter: ${count.value}`)
})

// Reactive changes
setTimeout(() => {
  count.value++                    // only updates what depends on count
  state.user.level += 10           // only updates what depends on level
}, 1000)
</script>
Enter fullscreen mode Exit fullscreen mode

Notice something? We didn’t have to tell Vue what to watch. It figured it out by itself.

The Curiosity Behind the Curtain

Vue’s reactivity system is so well isolated that you can actually use it standalone with the @vue/reactivity package — no Vue components required.
Internally:

reactive(obj) returns a Proxy that intercepts get and set operations.
While an "active effect" is running (component render, watchEffect, computed, etc.), every property read registers the current effect as a dependent.
Every write triggers only the relevant effects.

It’s a modern, automatic Observer Pattern that is incredibly efficient.

The Gotchas Every Vue Developer Discovers (Usually the Hard Way)

The magic isn’t perfect. Here are some interesting quirks:

1. reactive() only works with objects

JavaScript
const num = reactive(5) // ❌ Doesn't work! Use ref() for primitives
Enter fullscreen mode Exit fullscreen mode

2. Destructuring breaks reactivity

JavaScript
const state = reactive({ count: 0 })
const { count } = state   // count is now a plain number!

count++ // ❌ This does NOTHING reactive!

Enter fullscreen mode Exit fullscreen mode

Fix: Use toRefs() or always access via state.count.

  1. Adding new properties after creation
JavaScript
const state = reactive({ name: 'Bruno' })
state.age = 30 // ✅ This works! Vue tracks property additions
Enter fullscreen mode Exit fullscreen mode

However, operations like Object.assign() or delete can sometimes lose reactivity. Best practice: stick to direct property access or use shallowReactive when needed.
Why This Matters in 2026
With Vue 3.5+ bringing even better performance optimizations (especially for large arrays and reduced memory usage), understanding reactivity helps you write naturally performant code without excessive memoization.
It also makes composables incredibly powerful:

vue
<script setup>
// A reusable composable that automatically tracks dependencies
function useMousePosition() {
  const x = ref(0)
  const y = ref(0)

  // The watchEffect inside tracks dependencies automatically
  return { x, y }
}
</script>
Enter fullscreen mode Exit fullscreen mode

** Final Thoughts: Vue Makes Reactivity "Boringly Easy"**

The biggest curiosity about Vue.js is how it took something complex (dependency tracking and optimized updates) and made it feel obvious.
You don’t have to think about when your component should update. You just declare your state, and Vue takes care of the rest — with elegance, great performance, and a surprisingly small bundle size.
If you're still using the Options API or transitioning to Composition API, take some time to play with watchEffect, shallowRef, and understand how Proxies work under the hood. The feeling of control is addictive.

What about you?
What was the biggest "magic moment" — or the most surprising gotcha — you’ve encountered with Vue’s reactivity system?
Drop it in the comments below! 👇

Top comments (0)