Under The Hood
frontend vue Vue 3.x (Proxy-based)

Vue 3 reactivity: Proxy vs Vue 2 getter/setter

Last updated
Prerequisites:
Vue component basics
JavaScript Proxy and Object.defineProperty
  • vue
  • reactivity
  • proxy
  • defineproperty
  • vue3
  • javascript

Read at your depth

The practical view

Vue 2 wrapped object properties with Object.defineProperty, converting each existing key into a reactive getter/setter. That is why adding a new property to a reactive object required this.$set(obj, 'key', value) — the new key had no getter/setter, so it was invisible to reactivity. Vue 3 uses JavaScript Proxy: reactive(obj) returns a proxy that intercepts get, set, has, deleteProperty, and more, so brand-new keys are reactive automatically, no $set needed. In practice this means arrays indexed by number, added properties, and destructuring behavior all changed: obj['newKey'] = x just works, and arr[3] = v is tracked.

The same idea in other frameworks

react equivalent
const [obj, setObj] = useState({ count: 0 });
// updates are immutable: setObj({ ...obj, count: obj.count + 1 })

Legacy vs modern

Adding a key to a reactive object: $set vs direct assignment

Vue 2 required $set for new keys because defineProperty ran once at init; the Proxy in Vue 3 makes direct assignment reactive for any key, existing or new.

before → after
Vue 2 defineProperty
this.$set(this.user, 'role', 'admin');
// plain assignment would not trigger updates:
Vue 3 Proxy
state.user.role = 'admin'; // tracked by the set trap

Interview gotchas

Context

Interviewers ask this to check whether you understand defineProperty's initialization-time limitation versus Proxy's universal interception, not just that $set 'was annoying'.

The mechanical answer

Object.defineProperty must be called for each key at object-creation time to install the getter/setter. Vue 2 walked data() once; keys added later had no traps, so reads/writes bypassed tracking entirely. The ecosystem patched this with $set (which re-walks and redefines the key) and $delete. Vue 3's reactive() returns a Proxy whose set/get traps fire for every property access, so adding, deleting, and iterating are all intercepted natively. That also fixed the class of bugs where replace-with-array-index or length mutation were untracked in Vue 2.

Trap

A common trap is saying 'Vue 3 removed $set because proxies are faster'. Proxies are generally not faster than plain defineProperty access — the real win is correctness and universality (new keys, Maps/Sets, in-operator). Also, candidates sometimes claim reactive() mutates the original object; it does not — the original stays a raw object, and the proxy wraps it, which is why passing raw objects out of reactive can silently break tracking.

Under The Hood — a multi-depth technical interview hub.

Third-party licenses Press ⌘ K to search.