Skip to content

VueJS - Reactive Interface

Vue’s core feature is its reactivity system. When you modify reactive data, Vue automatically detects the change and updates the DOM efficiently. Vue 3 significantly revamped the reactivity system using JavaScript Proxies, making it more powerful and consistent than Vue 2’s Object.defineProperty-based system.

This chapter explores how reactivity works in Vue 3, how to declare reactive state using ref and reactive, and how to handle reactivity caveats.

  • Proxies: When you declare state using ref() or reactive(), Vue wraps your data (primitives or objects) in a JavaScript Proxy.
  • Tracking: When a component’s render function executes, Vue tracks which reactive properties are accessed (the ‘get’ trap of the Proxy).
  • Triggering: When a reactive property is modified (the ‘set’ trap of the Proxy), Vue checks which components depend on that property (tracked during rendering) and triggers them to re-render.

This Proxy-based system allows Vue 3 to track property additions and deletions for objects declared with reactive, overcoming limitations present in Vue 2.

ref() is used to create reactive variables for any value type, including primitives (String, Number, Boolean) and objects. It wraps the value in an object with a .value property.

import { ref } from 'vue';
// For primitives
const counter = ref(0);
const message = ref('Hello');
const isActive = ref(true);
console.log(counter.value); // Access/modify using .value in JS
counter.value++;
// For objects
const user = ref({ name: 'Alice', age: 30 });
user.value.age++; // Modify object property via .value
// In template, .value is automatically unwrapped:
// <p>{{ counter }}</p>
// <p>{{ user.name }}</p>

Use ref when you need reactivity for standalone primitive values or when you might need to reassign the entire object/array.

reactive() is used to create a reactive proxy specifically for objects (including arrays and built-in types like Map, Set). It makes the object itself deeply reactive.

import { reactive } from 'vue';
const state = reactive({
count: 0,
user: {
name: 'Bob',
isAdmin: false
},
items: ['apple', 'banana']
});
// Access/modify directly in JS
state.count++;
state.user.isAdmin = true;
state.items.push('orange'); // Array mutations are reactive
// In template:
// <p>{{ state.count }}</p>
// <p>{{ state.user.name }}</p>

Use reactive for grouping related data within a single object. Be aware that destructuring properties from a reactive object will cause them to lose reactivity unless you use utilities like toRefs.

  • Use ref for primitives and when you might reassign the whole variable.
  • Use reactive for complex objects where you’ll primarily modify properties within the object.
  • ref requires .value in script, reactive does not.
  • Both work seamlessly in templates (refs are auto-unwrapped).

Vue 3’s Proxy-based system handles most array and object manipulations reactively:

  • Mutation methods like push, pop, shift, unshift, splice, sort, reverse are tracked.
  • Directly setting an element by index (e.g., myArray.value[0] = newValue) is reactive.
  • Modifying the .length property is reactive.
  • Modifying existing properties is reactive.
  • Adding new properties to an object declared with reactive() is reactive in Vue 3 (e.g., state.newProp = 'some value'). This is a major improvement over Vue 2.
  • Deleting properties using the delete operator (e.g., delete state.oldProp) is reactive for objects declared with reactive().

Important Note: If you use ref() with an object (e.g., const myObj = ref({ a: 1 })), adding a new root-level property directly (myObj.value.b = 2) is reactive. Deleting (delete myObj.value.a) is also reactive.

The need for Vue 2’s Vue.set and Vue.delete is eliminated in Vue 3 due to the capabilities of Proxies.

Instead of manually watching properties outside the instance like vm.$watch in Vue 2, use the watch or watchEffect functions from the Composition API. These provide more explicit and powerful ways to react to state changes.

import { ref, watch, watchEffect } from 'vue';
const count = ref(0);
const data = reactive({ name: 'test', value: 10 });
// Watch a specific ref
watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`);
});
// Watch a getter function for a reactive property
watch(() => data.value, (newValue) => {
console.log(`Data value changed to ${newValue}`);
});
// Watch multiple sources
watch([count, () => data.value], ([newCount, newDataValue], [oldCount, oldDataValue]) => {
console.log('Count or data.value changed');
});
// watchEffect runs immediately and re-runs whenever its dependencies change
watchEffect(() => {
// Tracks count and data.name as dependencies automatically
console.log(`Current count: ${count.value}, Current name: ${data.name}`);
});
// Example usage
setTimeout(() => {
count.value = 5;
data.value = 20;
data.name = 'updated';
}, 1000);

watch provides fine-grained control over what to watch and gives access to old/new values. watchEffect is simpler for side effects that depend on multiple reactive properties, automatically tracking dependencies.

  • Destructuring reactive: Destructuring properties from a reactive object will cause them to lose reactivity because the destructured variable holds a plain value, not the proxy. Use toRefs or toRef to create refs for individual properties if needed.
  • import { reactive, toRefs } from ‘vue’;

const state = reactive({ foo: 1, bar: 2 });

// WRONG: foo and bar are not reactive here // let { foo, bar } = state;

// CORRECT: fooRef and barRef are refs, synced with state const { foo: fooRef, bar: barRef } = toRefs(state);

console.log(fooRef.value); // 1 state.foo++; console.log(fooRef.value); // 2

  • Replacing a reactive object: You cannot directly replace an entire object declared with reactive. If you need to replace the whole object, use ref instead.
  • import { reactive, ref } from ‘vue’;

let state = reactive({ count: 0 }); // WRONG: This breaks reactivity // state = reactive({ count: 1 });

// Instead, modify properties: // state.count = 1;

// OR: Use Object.assign if you need to merge objects // Object.assign(state, { count: 1, newProp: true });

// OR: Use ref if full replacement is needed let stateRef = ref({ count: 0 }); stateRef.value = { count: 1 }; // CORRECT: This works

Vue 3’s reactivity system, powered by Proxies and the ref/reactive APIs, offers a robust and intuitive way to build dynamic user interfaces. Understanding how it works and choosing the right API (ref vs reactive) helps in writing efficient and maintainable Vue applications.

Explore the official documentation for more advanced topics: Reactivity Fundamentals and Reactivity in Depth.