Not What ChangedâBut What Returned: Understanding How Vue Compares Old vs New in watch
Vueâs watch does not perform deep comparisons but instead compares the previous and current return values of a getter using identity, meaning it reacts to changes in references rather than inspecting internal structure unless dependency tracking is explicitly expanded with options like deep: true.
Itâs easy to assume what a watcher does.
You give it some data, change that data, and Vue tells you:
âThis changed.â
It feels like Vue is looking inside your state, comparing values, figuring out whatâs different.
But thatâs not whatâs happening.
Not even close.
The Illusion of âChange Detectionâ
Consider this:
watch(count, (newVal, oldVal) => {
console.log(newVal, oldVal)
})At first glance, it looks like Vue is watching count itself.
But itâs not watching the variable.
Itâs watching the result of a function.
Even when you pass count directly, Vue internally treats it like:
() => count.valueSo the real question becomes:
What does this function return now⌠compared to before?
Comparison Is Surprisingly Simple
Vue does not perform deep comparisons.
It does not inspect nested properties.
Instead, it does something much simpler:
It compares the new value and the old value using identity.
newVal !== oldValThatâs it.
If they are different, the watcher runs.
If they are the same, nothing happens.
When This Works Perfectly
With primitives, this feels natural:
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log(newVal, oldVal)
})When count changes from 0 to 1:
0 !== 1â trigger
Clean.
Predictable.
Exactly what you expect.
When It Starts to Break Expectations
Now consider an object:
const state = reactive({ count: 0 })
watch(() => state, () => {
console.log("changed")
})Then:
state.count++Nothing happens.
Why?
Because Vue compares:
newVal === oldValThey are the same object.
The structure changedâbut the reference did not.
And Vue only cares about the reference.
A Subtle Shift
At this point, something becomes clear:
Vue is not asking:
âWhat changed inside this object?â
It is asking:
âDid the result of this function change?â
Thatâs a completely different question.
The Role of deep: true
To handle nested changes, Vue offers:
watch(
() => state,
callback,
{ deep: true }
)But this is often misunderstood.
It does not mean:
âCompare deeply.â
Instead, it means:
âTouch everything so it can be tracked.â
Vue internally walks through the object, accessing every nested property.
And because tracking happens on access, it registers dependencies across the entire structure.
Now when state.count changes, the watcher runs.
But something interesting remains.
The Same Old Value
Even with deep: true:
watch(() => state, (newVal, oldVal) => {
console.log(newVal === oldVal) // true
})The watcher triggersâŚ
but both values are still the same object.
Vue never cloned anything.
It simply re-ran the function and noticed that something it depends on changed.
So What Is Actually Being Compared?
Not the object.
Not its contents.
Just the return value of the getter.
And for objects, that return value is the same reference.
A More Accurate Model
A watcher is not:
âWatch this dataâ
It is:
âRun this function, remember its result, and run it again later to see if the result is differentâ
Thatâs the entire system.
Where This Becomes Visible
Consider this:
watch(
() => ({ count: state.count }),
() => {
console.log("triggered")
}
)This function returns a new object every time.
So even if state.count hasnât changed, the result is a new reference.
Which means:
newVal !== oldValAlways true.
So the watcher always triggers.
Not because the data changedâ
but because the shape of the return value did.
Why Vue Chooses This Design
Deep comparison sounds nice.
But it comes with a cost:
- expensive traversal
- unpredictable performance
- unnecessary complexity
Instead, Vue chooses something simpler:
Compare cheaply, and track precisely
It doesnât try to understand your data.
It only observes how your code uses it.
A Final Thought
Watchers donât detect change in the way we often imagine.
They donât look inside values.
They donât compute differences.
They simply remember what a function returnedâ
and ask, later:
âIs this still the same thing?â
And sometimes, that question is much simplerâ
and much more powerfulâ
than trying to understand what changed at all.