---
title: Deep Watch Callback Receives Same Object Reference for Old and New Values
impact: MEDIUM
impactDescription: Comparing oldValue and newValue in deep watchers is misleading since they reference the same object
type: capability
tags: [vue3, watch, watchers, deep, oldValue, newValue, object-reference]
---

# Deep Watch Callback Receives Same Object Reference for Old and New Values

**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.

Don't rely on comparing `newValue` to `oldValue` in deep watchers for detecting what changed. Instead, track specific values or implement your own diffing.

## Task Checklist

- [ ] Don't compare newValue === oldValue in deep watchers to detect changes
- [ ] For change detection, watch specific properties instead
- [ ] If you need old values, manually snapshot before changes
- [ ] Consider using a serialization approach for complex diffing needs
- [ ] The values differ only when the entire object is replaced

**Incorrect:**

```javascript
import { reactive, watch } from 'vue';

const state = reactive({
  user: {
    name: 'John',
    preferences: { theme: 'dark' },
  },
});

// BAD: Trying to compare old and new values
watch(
  () => state.user,
  (newUser, oldUser) => {
    // This comparison is ALWAYS true for nested mutations!
    if (newUser === oldUser) {
      console.log('Same reference!'); // Always logs for nested changes
    }

    // This also won't work - they're the same object
    if (newUser.name !== oldUser.name) {
      console.log('Name changed'); // Never logs for nested mutations
    }
  },
  { deep: true },
);

// When this happens:
state.user.name = 'Jane';
// Both newUser and oldUser are { name: 'Jane', preferences: { theme: 'dark' } }
```

**Correct:**

```javascript
import { reactive, watch, ref } from 'vue';

const state = reactive({
  user: {
    name: 'John',
    preferences: { theme: 'dark' },
  },
});

// CORRECT: Watch specific properties you care about
watch(
  () => state.user.name,
  (newName, oldName) => {
    console.log(`Name changed from "${oldName}" to "${newName}"`);
    // oldName and newName are primitives, work correctly
  },
);

// CORRECT: Watch multiple specific properties
watch(
  [() => state.user.name, () => state.user.preferences.theme],
  ([newName, newTheme], [oldName, oldTheme]) => {
    if (newName !== oldName) {
      console.log(`Name: ${oldName} -> ${newName}`);
    }
    if (newTheme !== oldTheme) {
      console.log(`Theme: ${oldTheme} -> ${newTheme}`);
    }
  },
);
```

## Manual Snapshot Pattern

```javascript
import { reactive, watch, ref } from 'vue';

const state = reactive({ count: 0, items: [] });

// Keep a manual snapshot for comparison
const previousSnapshot = ref(JSON.stringify(state));

watch(
  state,
  (newState) => {
    const currentSnapshot = JSON.stringify(newState);

    if (currentSnapshot !== previousSnapshot.value) {
      const oldData = JSON.parse(previousSnapshot.value);
      console.log('Old:', oldData);
      console.log('New:', newState);

      // Update snapshot for next comparison
      previousSnapshot.value = currentSnapshot;
    }
  },
  { deep: true },
);
```

## When Old and New Values Differ

```javascript
import { reactive, watch } from 'vue';

const state = reactive({
  currentUser: { name: 'John' },
});

watch(
  () => state.currentUser,
  (newUser, oldUser) => {
    // THESE DIFFER when the object itself is replaced
    console.log('Old:', oldUser); // { name: 'John' }
    console.log('New:', newUser); // { name: 'Jane' }
  },
  { deep: true },
);

// Object replacement - old and new are different
state.currentUser = { name: 'Jane' };

// vs. Mutation - old and new are the same reference
// state.currentUser.name = 'Jane'
```

## Using Getter Returns New Object

```javascript
import { reactive, watch } from 'vue';

const state = reactive({
  user: { firstName: 'John', lastName: 'Doe' },
});

// CORRECT: Getter returns new object, so old/new comparison works
watch(
  () => ({ ...state.user }), // Shallow clone
  (newUser, oldUser) => {
    // Now these are different objects
    console.log('Changed from', oldUser, 'to', newUser);
  },
  { deep: true },
);
```

## Reference

- [Vue.js Watchers - Deep Watchers](https://vuejs.org/guide/essentials/watchers.html#deep-watchers)
