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>
97 lines
3.0 KiB
Markdown
97 lines
3.0 KiB
Markdown
---
|
|
title: Avoid Comparing Reactive Objects with === Operator
|
|
impact: HIGH
|
|
impactDescription: Reactive proxies have different identity than original objects - comparison bugs are silent and hard to debug
|
|
type: gotcha
|
|
tags: [vue3, reactivity, proxy, comparison, debugging, identity]
|
|
---
|
|
|
|
# Avoid Comparing Reactive Objects with === Operator
|
|
|
|
**Impact: HIGH** - Vue's `reactive()` returns a Proxy wrapper that has a different identity than the original object. Using `===` to compare reactive objects can lead to silent bugs where comparisons unexpectedly return `false`.
|
|
|
|
When you wrap an object with `reactive()`, the returned proxy is NOT equal to the original object. Additionally, accessing nested objects from a reactive object returns new proxy wrappers each time, which can cause identity comparison issues.
|
|
|
|
## Task Checklist
|
|
|
|
- [ ] Never compare reactive object instances with `===` directly
|
|
- [ ] Use unique identifiers (ID, UUID) for object comparison instead
|
|
- [ ] Use `toRaw()` on both sides when identity comparison is absolutely necessary
|
|
- [ ] Consider using primitive identifiers from database records for comparison
|
|
|
|
**Incorrect:**
|
|
```javascript
|
|
import { reactive } from 'vue'
|
|
|
|
const original = { id: 1, name: 'Item' }
|
|
const state = reactive(original)
|
|
|
|
// BUG: Always returns false - proxy !== original
|
|
if (state === original) {
|
|
console.log('Same object') // Never executes
|
|
}
|
|
|
|
// BUG: Nested object comparison fails
|
|
const items = reactive([{ id: 1 }, { id: 2 }])
|
|
const item = items[0]
|
|
|
|
// Later...
|
|
if (items[0] === item) {
|
|
// May or may not work depending on Vue's proxy caching
|
|
}
|
|
|
|
// BUG: Comparing items from different reactive sources
|
|
const listA = reactive([{ id: 1 }])
|
|
const listB = reactive([{ id: 1 }])
|
|
if (listA[0] === listB[0]) {
|
|
// Never true, even though they represent the same data
|
|
}
|
|
```
|
|
|
|
**Correct:**
|
|
```javascript
|
|
import { reactive, toRaw } from 'vue'
|
|
|
|
const original = { id: 1, name: 'Item' }
|
|
const state = reactive(original)
|
|
|
|
// CORRECT: Use toRaw() for identity comparison
|
|
if (toRaw(state) === original) {
|
|
console.log('Same underlying object') // Works!
|
|
}
|
|
|
|
// BEST: Use unique identifiers instead
|
|
const items = reactive([
|
|
{ id: 'uuid-1', name: 'Item 1' },
|
|
{ id: 'uuid-2', name: 'Item 2' }
|
|
])
|
|
|
|
function findItem(targetId) {
|
|
return items.find(item => item.id === targetId)
|
|
}
|
|
|
|
function isSelected(item) {
|
|
return selectedId.value === item.id // Compare IDs, not objects
|
|
}
|
|
|
|
// CORRECT: For Set/Map operations, use primitive keys
|
|
const selectedIds = reactive(new Set())
|
|
selectedIds.add(item.id) // Use ID, not object
|
|
selectedIds.has(item.id) // Check by ID
|
|
```
|
|
|
|
```javascript
|
|
// When you must compare objects, use toRaw on both sides
|
|
import { toRaw, isReactive } from 'vue'
|
|
|
|
function areEqual(a, b) {
|
|
const rawA = isReactive(a) ? toRaw(a) : a
|
|
const rawB = isReactive(b) ? toRaw(b) : b
|
|
return rawA === rawB
|
|
}
|
|
```
|
|
|
|
## Reference
|
|
- [Vue.js Reactivity in Depth](https://vuejs.org/guide/extras/reactivity-in-depth.html)
|
|
- [Vue.js toRaw() API](https://vuejs.org/api/reactivity-advanced.html#toraw)
|