Pulled ALL skills from 15 source repositories: - anthropics/skills: 16 (docs, design, MCP, testing) - obra/superpowers: 14 (TDD, debugging, agents, planning) - coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth) - better-auth/skills: 5 (auth patterns) - vercel-labs/agent-skills: 5 (React, design, Vercel) - antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo) - Plus 13 individual skills from various repos Mosaic Stack is not limited to coding — the Orchestrator and subagents serve coding, business, design, marketing, writing, logistics, analysis, and more. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
166 lines
4.3 KiB
Markdown
166 lines
4.3 KiB
Markdown
---
|
|
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)
|