How Vue 2 Knows When Your Data Changes
Vue 2's reactivity system converts plain data objects into observable structures using Object.defineProperty. Understanding exactly how that conversion works, and where it cannot reach, determines how you write components that update correctly.
Vue 2 updates the DOM automatically when data changes, but that automatic behaviour rests on a specific mechanism applied at instance creation time. When that mechanism cannot reach a piece of data, the update does not happen. Knowing how the system is built explains both why it works reliably in the common case and where it silently fails.
Object.defineProperty and the Observer
When a Vue instance is created, Vue walks every property in the data object and rewrites each one using Object.defineProperty. The plain property is replaced with a getter and a setter pair. The getter records which component is currently rendering as a dependency of that property. The setter notifies those recorded dependents whenever the value is assigned a new value.
// Conceptual representation of what Vue does internally
Object.defineProperty(vm.someObject, 'message', {
get() {
// Track the current watcher as a dependency
dep.depend()
return value
},
set(newValue) {
value = newValue
// Notify all watchers that depend on this property
dep.notify()
}
})
Each component instance has a watcher. During rendering, as the template reads properties, the getters fire and the watcher is registered as a dependent. When any of those properties is later assigned, the setter fires, the watcher is notified, and the component schedules a re-render. The three actors involved are the Observer, which installs the getter and setter pairs on the data object, the Dep, which is a dependency list attached to each reactive property, and the Watcher, which is the consumer that re-runs when notified.
This conversion happens once, during new Vue(...) or when a component is initialised. Properties that exist in data at that moment become reactive. Properties introduced afterward do not.
Why Vue 2 Cannot Detect Property Addition or Deletion
Because the getter and setter are installed at initialisation, a property added to an object after the Vue instance is created has no getter or setter. Vue has no way to intercept reads or writes to it, and it is therefore not reactive.
export default {
data() {
return {
user: { name: 'Ada' }
}
},
methods: {
addAge() {
// This assignment does NOT trigger reactivity
this.user.age = 30
}
}
}
After this.user.age = 30, the template will not update even if it references user.age, because no setter was ever installed for that property. The same is true of deletion: removing a property with delete bypasses the setter entirely, so Vue cannot observe the removal.
The consequence for component authoring is that the initial shape of every data object must be declared in full inside data(), even when some properties start with empty or null values. Declaring age: null upfront ensures Vue installs a setter for it before the component mounts.
Why Array Index Assignment and Length Mutation Are Not Reactive
Vue 2 cannot detect two specific patterns of array mutation. Directly assigning by index and setting the length property do not trigger updates.
// Neither of these will cause a re-render:
this.items[2] = 'new value'
this.items.length = 0
This is a direct consequence of Object.defineProperty. The property it converts is the array’s index key, but JavaScript arrays allocate index properties dynamically. Installing setters in advance for all possible future indices is not practical, and reducing length does not write to any individual keyed property. Vue has no interception point for either operation.
Reactive Array Methods
Vue 2 wraps the seven array mutation methods that modify the array in place and replaces them on any array it observes. These are push, pop, shift, unshift, splice, sort, and reverse. When one of these methods is called, Vue’s wrapped version performs the original mutation and then notifies dependents.
// All of these trigger reactivity:
this.items.push('new item')
this.items.pop()
this.items.splice(1, 1, 'replacement')
this.items.sort()
this.items.reverse()
Non-mutating methods such as filter, concat, and slice return new arrays rather than modifying the original. Replacing the reactive array reference with the returned value triggers the setter on the parent object, which is reactive, so those patterns work correctly.
// Replacing the array is reactive because it triggers the setter on `this.items`
this.items = this.items.filter(item => item.active)
Vue.set and vm.$set
Vue.set and its instance alias this.$set exist specifically to work around the property addition limitation. They add a property to a reactive object and ensure it is made reactive before the assignment, then trigger a dependency notification.
// Adding a reactive property to a nested object
Vue.set(this.user, 'age', 30)
this.$set(this.user, 'age', 30)
For arrays, both methods accept an index as the second argument, which makes them the correct way to set a value by index reactively.
// Setting an array item by index reactively
Vue.set(this.items, 2, 'new value')
this.$set(this.items, 2, 'new value')
// Equivalent using splice:
this.items.splice(2, 1, 'new value')
When assigning several properties to a reactive object at once, the correct approach is to create a fresh object rather than assigning individual keys through a loop. Assigning through Object.assign into the existing object does not make the new keys reactive, because Object.assign bypasses Vue’s setters.
// Incorrect: new keys on this.user are not reactive
Object.assign(this.user, { age: 30, role: 'admin' })
// Correct: replace the object reference with a new merged object
this.user = Object.assign({}, this.user, { age: 30, role: 'admin' })
The Practical Caveats
The constraints above create a set of patterns to avoid and a set of equivalents to use instead. Assigning to an undeclared property inside a method will not cause a re-render even if the template references it. Resetting an array by writing this.items.length = 0 will not clear the rendered list. Filling an array by index in a loop will not produce any updates. The components appear to ignore those operations because, from Vue’s perspective, nothing changed.
Declared-upfront initialisation is the primary mitigation. Using splice instead of index assignment, and Vue.set when object shapes must grow after initialisation, covers the remaining cases. The limitation is not a runtime error but a silent failure to update the view, which makes it a class of bug that can persist unnoticed if the data patterns are not written with the underlying mechanism in mind.
What You Can Do Now
Create a minimal Vue 2 instance and exercise each of the patterns described above to observe the reactive and non-reactive behaviours directly.
<div id="app">
<p>Name: {{ user.name }}</p>
<p>Age: {{ user.age }}</p>
<ul>
<li v-for="(item, i) in items" :key="i">{{ item }}</li>
</ul>
</div>
<script>
const vm = new Vue({
el: '#app',
data: {
user: { name: 'Ada', age: null },
items: ['alpha', 'beta', 'gamma']
}
})
// Reactive: age was declared in data(), setter exists
vm.user.age = 30
// Not reactive: surname was not declared in data()
vm.user.surname = 'Lovelace'
// Reactive: Vue.set installs a setter and notifies
Vue.set(vm.user, 'surname', 'Lovelace')
// Not reactive: direct index assignment
vm.items[0] = 'replaced'
// Reactive: splice triggers Vue's wrapped method
vm.items.splice(0, 1, 'replaced')
// Not reactive: length assignment
vm.items.length = 1
// Reactive: splice with no replacement items to truncate
vm.items.splice(1)
</script>
Run this in a browser with Vue 2 loaded from a CDN. After each operation, inspect the rendered output to confirm which assignments updated the view and which did not. The difference between a declared property and an undeclared one, and between splice and index assignment, becomes apparent immediately.