Vue 3 watch's oldValue and newValue Point to the Same Proxy: Snapshot Strategies for ref and reactive
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.
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.
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.
Deep watching is not the same as preserving an old snapshot.