In Vue 3, why is `reactive(obj) === obj` false, and what do repeated reactive() calls on the same object or its proxy return?
answer
- a wrapper, not the object
- one wrapper per target
- cached in a WeakMap
- the original is left untracked
basics
~10 sreactive(obj) returns a Proxy wrapping obj, a different object, so === is false. Vue caches one proxy per target: calling reactive(obj) again returns that same proxy, and reactive(proxy) returns the proxy itself.
solid answer
~40 s`reactive(obj)` does not modify `obj`; it creates a `Proxy` whose target is `obj` and returns that, so `reactive(obj) === obj` is `false`. To keep identity stable, Vue stores each proxy in a `WeakMap` keyed by its target: `reactive(obj)` a second time returns the same proxy, and `reactive(proxy)` detects that it already has a proxy and returns it unchanged. Nested objects follow the same rule — `state.nested` is the cached proxy of the raw nested object, so it is stable across reads but never equal to the raw one. Only writes through the proxy are seen; mutating `obj` directly changes the data but triggers nothing. `isReactive()` and `isProxy()` tell you which kind of value you hold, and `toRaw()` gets the original back.
code
ts · 18 linesimport { reactive, readonly, isReactive, isProxy, toRaw } from 'vue'
const raw = { count: 0, nested: { n: 1 } }
const p = reactive(raw)
p === raw // false: p is a Proxy
reactive(raw) === p // true: cached per target
reactive(p) === p // true: already a proxy
p.nested === p.nested // true: nested proxy cached too
p.nested === raw.nested // false
toRaw(p) === raw // true
raw.count = 5 // data changes, but nothing is triggered
p.count // 5
const view = readonly({ a: 1 })
isProxy(view) // true
isReactive(view) // falsego deeper
Remember that reactive() hands back a proxy that is not equal to the object you passed in, and that you should only use the proxy afterwards.
Explain the per-target proxy cache, why reactive(proxy) returns itself, lazy nested proxies, and what isReactive, isProxy and toRaw each report.
Diagnose identity bugs where raw and proxied references meet, and choose between toRaw comparisons, id comparisons and proxy-only discipline.
Set conventions that keep raw references from leaking into state, especially at boundaries with third-party code that compares by reference.
## A wrapper, not a conversion In Vue 3, `reactive(obj)` does not change `obj` at all. It creates a JavaScript `Proxy` — a separate object that forwards operations to a *target* — with `obj` as the target, and returns that proxy. The proxy's handlers record reads and announce writes; the target just holds the data. Two different objects therefore exist: - the **raw** object you passed in, which Vue leaves as it was; - the **reactive proxy**, which is what Vue tracks. So `reactive(obj) === obj` is `false`, and that is by design. ## One proxy per object If every call created a new proxy, the same data would have several identities and comparisons would be hopeless. Vue prevents that with a cache: 1. `reactive(obj)` looks `obj` up in a `WeakMap` from targets to proxies; if a proxy exists, it is returned. 2. `reactive(proxy)` notices that its argument is already a Vue proxy and returns it as is. 3. Nested objects are converted **lazily** when read through a proxy, using the same cache, so `state.user === state.user` is `true`. | Expression | Result | |---|---| | `reactive(obj) === obj` | `false` | | `reactive(obj) === reactive(obj)` | `true` | | `reactive(p) === p` for a reactive proxy `p` | `true` | | `p.nested === p.nested` | `true` | | `p.nested === obj.nested` | `false` | | `readonly(p) === p` | `false` — a readonly view is its own proxy | The `WeakMap` means the cache does not keep raw objects alive once nothing else references them. ## Why writes to the original are invisible Only the proxy's handlers know about effects. Writing `obj.count = 5` changes the data — `p.count` reads `5` afterwards, because the proxy forwards to the same target — but no effect is triggered, so a component that rendered `p.count` does not update until something else re-renders it. The rule the docs give is to **use the proxied version exclusively** once state is reactive. ## Telling them apart - `isReactive(x)` — `true` for proxies from `reactive()` or `shallowReactive()`, and for a `readonly()` view of a reactive proxy; - `isReadonly(x)` — `true` for proxies from `readonly()` or `shallowReadonly()`; - `isProxy(x)` — `true` for any of those four kinds; - `toRaw(x)` — returns the original object behind any of them, unwrapping layers until it reaches a plain object, and returns non-proxies unchanged. A `readonly()` view of a plain object is a proxy but not reactive: `isProxy` is `true`, `isReactive` is `false`. ## Where identity bites Identity bugs appear wherever code compares an object it kept from before (raw) with one it read from state (a proxy): a selected item held in a ref, a lookup in a plain array or Set outside Vue, an equality check in a third-party library. The fixes are to compare through `toRaw()` on both sides, compare by a stable id, or keep only proxies. ## Why interviewers ask It checks whether a candidate understands that Vue 3's reactivity is a wrapper around data rather than a mutation of it — the opposite of Vue 2, which rewrote the original object's properties in place.
- In Vue 3, what does isReactive() return for readonly(reactive(obj)) compared with readonly(obj)?`true` for `readonly(reactive(obj))`, because `isReactive` looks through a readonly proxy to the reactive one underneath; `false` for `readonly(obj)`, which is a proxy over a plain object. `isProxy` is `true` for both.
- Why does Vue cache proxies in a WeakMap rather than a Map?The cache is keyed by the raw object. A `WeakMap` holds its keys weakly, so once nothing else references a raw object, it and its cached proxy can be garbage-collected; a `Map` would keep every object ever made reactive alive.
A receptionist in front of an office: every request that goes through the receptionist is logged and the right people are notified, and you always get the same receptionist for the same office. Someone who walks in through the back door (the raw object) changes things just as well, but nobody is told.
saying these in an interview costs you the question
- reactive() makes the original object itself reactive, as in Vue 2
- Each reactive() call on the same object creates a new proxy
- reactive(proxy) wraps an existing proxy in a second proxy
- Writing to the raw object also triggers component updates
- isProxy and isReactive always return the same result