跪拜 Guibai
← All articles
Vue.js · Frontend

Vue 3 watch's oldValue and newValue Point to the Same Proxy: Snapshot Strategies for ref and reactive

By 星光不问赶路人 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

State diffing, undo/redo, and change auditing in Vue 3 depend on comparing states, and watch's oldValue is unreliable for in-place mutations. Snapshot patterns turn that into a predictable task, but the wrong choice means silent self-comparisons or expensive deep clones on every change.

Summary

Vue 3's watch does not keep history. A watcher on a ref holding an object fires on replacement of .value by default, and replacing the whole object yields distinct old and new objects. Editing a nested property requires { deep: true }, and then the callback receives the same proxy as both arguments. A watcher on a reactive object is deep by default, so property edits trigger it, but oldValue and newValue still point to the same proxy; reassigning the reactive variable triggers nothing.

Snapshots have to be built deliberately. A computed(() => ({ ...obj })) shallow-copies a flat reactive object and gives the watcher real old and new values. Nested data needs a deep copy, and structuredClone cannot clone a Proxy directly, so the reliable pattern is a manual watch that keeps a previousSnapshot variable and clones toRaw(obj) on every change. The practical conclusion follows from those constraints: watch a specific field when possible, use ref whole-object assignment for replacement semantics, use computed snapshots for small objects, and use watch plus deep copy for large nested reactive state.

Takeaways
— watch on a ref observes replacement of .value by default; nested property edits need { deep: true }.
— A reactive object's watcher is deep by default, so property mutations fire the callback, but oldValue and newValue remain the same proxy.
— Reassigning a reactive variable to a new value doesn't trigger a watcher on that variable.
— Whole-object assignment to a ref (aaa.value = { ...aaa.value }) triggers the watcher and gives independent old and new objects.
— Flat reactive objects can be snapshotted with computed(() => ({ ...bbb })), which isolates only the first layer.
— Nested reactive objects need a deep copy; the recommended pattern keeps a previousSnapshot variable and runs structuredClone(toRaw(bbb)) inside the watch callback.
— structuredClone cannot clone a Proxy directly, and toRaw inside a computed can break dependency tracking.
— Watching a specific field is the cheapest option when one exists.
Conclusions

In-place mutation makes oldValue and newValue the same object, so any comparison between them before a change was captured will silently report equality; watchers need to snapshot state before the mutation.

ref and reactive trade different watch semantics: ref gives clean old and new values only on whole-object replacement, while reactive gives deep-change notifications but no historical identity, so choosing one is also choosing a snapshot strategy.

Because structuredClone rejects Proxy objects and toRaw inside computed risks losing reactivity, simple deep-copy solutions fail; the workaround is manual bookkeeping inside watch, which puts a snapshot cost on every change.

Concepts & terms
ref
A Vue 3 API that wraps a value in an object with a .value property. When the value is an object, watch by default only tracks replacement of that .value, not mutations inside it.
reactive
A Vue 3 API that returns a Proxy around an object. Watchers on the proxy are deep by default, so property mutations trigger callbacks, but oldValue and newValue stay the same proxy instance.
shallow snapshot
A copy created by spreading an object into a new one, capturing only top-level properties. It is enough for flat state but does not isolate nested objects.
toRaw
A Vue 3 utility that returns the original non-reactive object behind a reactive proxy, allowing operations such as structuredClone that cannot handle Proxy instances.
From the discussion
Featured comments
卷土的土

Deep watching is not the same as preserving an old snapshot.

See top comments, translated →
Source: juejin.cn ↗ Google Translate ↗ Backup ↗