skip to content

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