skip to content

Getter-Setter Reactivity

Vue 2 made data reactive with Object.defineProperty, so added or deleted properties and array index writes went unseen without Vue.set. Interviewers ask why proxies fixed this and cost IE11.

part ofVue.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

3

In Vue 2, why did `this.user.nickname = 'Ace'`, a key missing from `data`, not update the view, and how did `Vue.set` fix it?

level: middleimportance: must knowfreq 68%

answer

  1. reactivity installed once, at creation
  2. accessors are per key, not per object
  3. a later key has no setter
  4. define it, then notify the parent object

basics

~20 s

Vue 2 turned each property that existed at creation into an Object.defineProperty getter/setter. A key added later is a plain property with no setter, so the write goes unseen; Vue.set(obj, key, value), or this.$set, adds it reactively and notifies watchers.

solid answer

~50 s

Vue 2 walked `data` when the component was created and replaced every existing property with an `Object.defineProperty` getter/setter: the getter recorded which watchers read it, the setter notified them. That conversion is per key, so a key added afterwards, like `nickname` on a `user` that started as `{ name: 'Ann' }`, is an ordinary property with no setter and nothing is notified. The value is really stored, so it appears as soon as something unrelated re-renders the component, which is why the bug looks intermittent. The fixes were `this.$set(this.user, 'nickname', 'Ace')` (global `Vue.set`), declaring the key up front as `nickname: null`, or replacing the object with a fresh copy. Deleting a key had the mirror problem and needed `Vue.delete`. Vue 3's Proxy-based reactivity sees added and deleted keys, so `Vue.set`, `Vue.delete` and their `this.$set`/`this.$delete` aliases were removed.

code

js · 20 lines
js
// Vue 2 Options API
export default {
  data() {
    return { user: { name: 'Ann' } }
  },
  methods: {
    broken() {
      this.user.nickname = 'Ace' // stored, but no watcher is notified
    },
    withSet() {
      this.$set(this.user, 'nickname', 'Ace') // reactive key + notify
    },
    withReplace() {
      this.user = { ...this.user, nickname: 'Ace' } // user setter fires
    },
    removeKey() {
      this.$delete(this.user, 'nickname') // plain delete would be silent
    }
  }
}

go deeper

for a junior

Remember the symptom and the fix: in Vue 2 a key added after the component was created does not update the view until you use this.$set or declare the key in data.

for a middle

Explain the per-key Object.defineProperty conversion at creation, why a later key has no setter, and how Vue.set installs one and notifies the parent object's dependency list.

for a senior

Show you can diagnose it from symptoms, a value correct in the console but stale on screen until an unrelated re-render, and choose between declaring the shape, Vue.set, or replacing the object.

for a principal

Frame it as the constraint behind Vue 3's rewrite: an ES5-compatible engine forced state to be declared up front, and removing that constraint with Proxies cost IE11 support.

## How Vue 2 made data reactive When a Vue 2 component was created, Vue took the object returned by `data()` and **walked it recursively**. For every property that existed at that moment it called `Object.defineProperty` to replace the plain value with an **accessor pair**: - the **getter** recorded which watcher was reading the value (the component's render watcher, a `computed`, a `watch`) and added it to that key's dependency list; - the **setter** stored the new value and **notified** every recorded watcher, which queued a re-render. Each observed object also carried a hidden observer (`__ob__`) holding an **object-level dependency list**, so a watcher that read `this.user` was subscribed both to the `user` key and to the `user` object as a whole. That second list matters for the fix below. The crucial property of this design: conversion happened **once, for the keys that existed**. The ES5 feature set Vue 2 targeted offered no way to intercept the *creation* of a property that did not exist yet. ## Why the new key was invisible ```js export default { data() { return { user: { name: 'Ann' } } }, methods: { addNickname() { this.user.nickname = 'Ace' // plain property: no setter, no notification } } } ``` `nickname` is created as an ordinary data property. The assignment succeeds and the value is really there, but no setter ran, so no watcher was queued. A template showing `{{ user.nickname }}` keeps its old output **until something unrelated re-renders the component** (typing in another field, a prop change), and then the value suddenly appears. That delay is why the bug is so often reported as intermittent, or as "it updates when I click somewhere else". Removing a key with `delete this.user.nickname` had the mirror-image problem: the key vanished without anyone being told. Typical symptoms when you meet it in an old codebase: - the value is correct in the console or in Vue Devtools, but the screen is stale; - it shows up after an unrelated interaction; - the key is missing from the object's initial shape in `data()`. ## What Vue.set and Vue.delete did `Vue.set(target, key, value)` (instance alias `this.$set`) did what a plain assignment could not: 1. If the key already existed on the object, it simply assigned it, so the existing setter fired. 2. Otherwise it installed a reactive getter/setter for the new key. 3. It then notified the object's **object-level** dependency list, so every watcher that had read the parent object re-ran and picked up the new key. `Vue.delete(target, key)` (`this.$delete`) deleted the key and notified the same list. On arrays both helpers used `splice` under the hood. Two limits applied: they refused to add reactive keys to a component instance or to its root `data` object (root keys had to be declared in `data`), and on a plain object Vue had never observed they just assigned or deleted without notifying anyone. ## The three fixes, compared | Fix | Example | When it fits | |---|---|---| | `Vue.set` / `this.$set` | `this.$set(this.user, 'nickname', 'Ace')` | The key is genuinely dynamic, such as user-defined fields or map-like objects | | Declare the key up front | `user: { name: 'Ann', nickname: null }` | The shape is known in advance; simplest and self-documenting | | Replace the object | `this.user = { ...this.user, nickname: 'Ace' }` | Adding several keys at once; the `user` setter fires and the new object is converted | A common half-fix is `Object.assign(this.user, { nickname: 'Ace' })`: it mutates the same object, so no setter sees a change and the new key is still plain. Assigning into a **fresh** object is what makes the replacement approach work. ## What changed in Vue 3 Vue 3 makes `data()` and `reactive()` state reactive with **Proxies**, which intercept writes to keys that do not exist yet as well as `delete`. The plain assignment in the example now updates the view, so `Vue.set`, `Vue.delete`, `vm.$set` and `vm.$delete` were **removed** as no longer needed. The migration build (`@vue/compat`) keeps them as plain assignments or deletions that log a deprecation warning. When you meet this bug today it is in a Vue 2 codebase, including Vue 2.7: 2.7 backported the Composition API but kept the getter/setter engine and its caveats.

  • Does `Object.assign(this.user, { nickname: 'Ace' })` fix the stale view in Vue 2?
    No. `Object.assign` mutates the existing object and adds `nickname` as a plain property, so no setter runs. Even `this.user = Object.assign(this.user, ...)` fails, because the `user` setter receives the same object and skips notifying when the value is unchanged. Copying into a fresh object, `Object.assign({}, this.user, { nickname: 'Ace' })` or a spread, works: the setter sees a new object and converts all of its keys.
  • Why could you not use `this.$set(this, 'nickname', 'Ace')` to add a new top-level key to a Vue 2 component?
    Vue 2 refused to add reactive properties to a component instance or its root `data` object, and warned in development. Root keys were set up when the component was created, and `data` was expected to declare the component's full state shape. The fix is to declare the key in `data`, even as `null`, and reserve `Vue.set` for keys on nested objects.

Vue 2 fitted an alarm sensor to every door the house had on move-in day. A door knocked through later opens and closes perfectly well, but no alarm sounds until someone calls the installer back, which is what Vue.set did for a new key.

saying these in an interview costs you the question

  • Vue 2 tracked whole objects, so any new key was reactive automatically
  • Calling this.$forceUpdate() makes a newly added key reactive
  • Object.assign(this.user, extra) triggers an update in Vue 2
  • Vue.set is still needed in Vue 3 for keys added after creation
  • The async update queue lost the write, so it needs nextTick
open as a page

In Vue 2, why did `this.items[2] = 'x'` and `this.items.length = 0` leave the view stale, and what did you write instead?

level: juniorimportance: should knowfreq 48%

basics

~20 s

Vue 2 saw array changes only through its wrapped mutation methods: push, pop, shift, unshift, splice, sort and reverse. Index assignment and length writes bypass them. Use splice(2, 1, 'x') or this.$set(items, 2, 'x'), and splice(0) or a new array to empty it.

open as a page

Why did Vue 3 replace Vue 2's Object.defineProperty reactivity with Proxies, which caveats disappeared, and why did that end IE11 support?

level: seniorimportance: should knowfreq 52%

basics

~20 s

Getter/setters only cover keys that exist when converted, so Vue 2 missed added or deleted keys and array index or length writes. Proxies trap every key, making Vue.set unnecessary, but Proxy cannot be polyfilled, so Vue 3 cannot run on IE11.

open as a page