Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Vue 3 debugging reference for reactivity issues, computed errors, watcher bugs, async failures, and SSR hydration problems.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
reference/watch-deep-same-object-reference.md
1---2title: Deep Watch Callback Receives Same Object Reference for Old and New Values3impact: MEDIUM4impactDescription: Comparing oldValue and newValue in deep watchers is misleading since they reference the same object5type: capability6tags: [vue3, watch, watchers, deep, oldValue, newValue, object-reference]7---89# Deep Watch Callback Receives Same Object Reference for Old and New Values1011**Impact: MEDIUM** - When using deep watchers on reactive objects, both `newValue` and `oldValue` in the callback point to the same object reference. They will always be equal for nested mutations because Vue doesn't clone the object before mutation.1213Don't rely on comparing `newValue` to `oldValue` in deep watchers for detecting what changed. Instead, track specific values or implement your own diffing.1415## Task Checklist1617- [ ] Don't compare newValue === oldValue in deep watchers to detect changes18- [ ] For change detection, watch specific properties instead19- [ ] If you need old values, manually snapshot before changes20- [ ] Consider using a serialization approach for complex diffing needs21- [ ] The values differ only when the entire object is replaced2223**Incorrect:**24```javascript25import { reactive, watch } from 'vue'2627const state = reactive({28user: {29name: 'John',30preferences: { theme: 'dark' }31}32})3334// BAD: Trying to compare old and new values35watch(36() => state.user,37(newUser, oldUser) => {38// This comparison is ALWAYS true for nested mutations!39if (newUser === oldUser) {40console.log('Same reference!') // Always logs for nested changes41}4243// This also won't work - they're the same object44if (newUser.name !== oldUser.name) {45console.log('Name changed') // Never logs for nested mutations46}47},48{ deep: true }49)5051// When this happens:52state.user.name = 'Jane'53// Both newUser and oldUser are { name: 'Jane', preferences: { theme: 'dark' } }54```5556**Correct:**57```javascript58import { reactive, watch, ref } from 'vue'5960const state = reactive({61user: {62name: 'John',63preferences: { theme: 'dark' }64}65})6667// CORRECT: Watch specific properties you care about68watch(69() => state.user.name,70(newName, oldName) => {71console.log(`Name changed from "${oldName}" to "${newName}"`)72// oldName and newName are primitives, work correctly73}74)7576// CORRECT: Watch multiple specific properties77watch(78[() => state.user.name, () => state.user.preferences.theme],79([newName, newTheme], [oldName, oldTheme]) => {80if (newName !== oldName) {81console.log(`Name: ${oldName} -> ${newName}`)82}83if (newTheme !== oldTheme) {84console.log(`Theme: ${oldTheme} -> ${newTheme}`)85}86}87)88```8990## Manual Snapshot Pattern9192```javascript93import { reactive, watch, ref } from 'vue'9495const state = reactive({ count: 0, items: [] })9697// Keep a manual snapshot for comparison98const previousSnapshot = ref(JSON.stringify(state))99100watch(101state,102(newState) => {103const currentSnapshot = JSON.stringify(newState)104105if (currentSnapshot !== previousSnapshot.value) {106const oldData = JSON.parse(previousSnapshot.value)107console.log('Old:', oldData)108console.log('New:', newState)109110// Update snapshot for next comparison111previousSnapshot.value = currentSnapshot112}113},114{ deep: true }115)116```117118## When Old and New Values Differ119120```javascript121import { reactive, watch } from 'vue'122123const state = reactive({124currentUser: { name: 'John' }125})126127watch(128() => state.currentUser,129(newUser, oldUser) => {130// THESE DIFFER when the object itself is replaced131console.log('Old:', oldUser) // { name: 'John' }132console.log('New:', newUser) // { name: 'Jane' }133},134{ deep: true }135)136137// Object replacement - old and new are different138state.currentUser = { name: 'Jane' }139140// vs. Mutation - old and new are the same reference141// state.currentUser.name = 'Jane'142```143144## Using Getter Returns New Object145146```javascript147import { reactive, watch } from 'vue'148149const state = reactive({150user: { firstName: 'John', lastName: 'Doe' }151})152153// CORRECT: Getter returns new object, so old/new comparison works154watch(155() => ({ ...state.user }), // Shallow clone156(newUser, oldUser) => {157// Now these are different objects158console.log('Changed from', oldUser, 'to', newUser)159},160{ deep: true }161)162```163164## Reference165- [Vue.js Watchers - Deep Watchers](https://vuejs.org/guide/essentials/watchers.html#deep-watchers)166