Demystifying Vue 3 Reactivity - A Deep Dive into ref, reactive, and effect
Emily Parker
Product Engineer · Leapcell

Introduction
In the world of modern web development, creating dynamic and responsive user interfaces is paramount. Frameworks like Vue.js have significantly simplified this process, offering powerful tools for managing application state and rendering updates efficiently. At the heart of Vue 3's elegance lies its refined reactivity system, a sophisticated mechanism that automatically tracks dependencies and re-renders components only when necessary. Understanding this system is crucial for writing performant, maintainable, and predictable Vue applications. This article will thoroughly explore the underlying principles of Vue 3's reactivity, focusing on the fundamental building blocks: ref
, reactive
, and effect
.
Core Concepts of Reactivity
Before we dive into the specific APIs, let's establish a foundational understanding of the key concepts that underpin Vue 3's reactivity system.
Reactive State: This refers to any data that, when changed, should potentially trigger an update to the user interface. In Vue, you explicitly declare certain data as reactive.
Dependency Tracking: The system's ability to 'know' which parts of your code (e.g., template renders, computed properties) rely on specific pieces of reactive state.
Change Detection: The mechanism that monitors for modifications to reactive state.
Side Effect (or Effect): Any operation that reads reactive state and might need to be re-executed when that state changes. The most common effect is rendering the component's template.
With these concepts in mind, let's explore how ref
and reactive
create reactive state, and how effect
orchestrates the updates.
Creating Reactive State
Vue 3 provides two primary ways to declare reactive state: ref
and reactive
. Their choice often depends on the type of data you're dealing with.
ref
for Primitive Values
ref
is primarily used for creating reactive references to primitive values like numbers, strings, and booleans, or even single objects. It wraps the value in an object with a .value
property. This wrapper allows the reactivity system to track changes to the underlying primitive.
import { ref } from 'vue'; // Creating a reactive number const count = ref(0); console.log(count.value); // 0 // Modifying the reactive number count.value++; console.log(count.value); // 1 // Example in a Vue component // <template> // <p>Count: {{ count }}</p> // <button @click="count++">Increment</button> // </template> // <script setup> // import { ref } from 'vue'; // const count = ref(0); // </script>
When count.value
is accessed in a template or an effect
, Vue's reactivity system "registers" a dependency. When count.value
is subsequently modified, the system "notifies" all registered dependencies, triggering their re-execution.
reactive
for Objects and Arrays
reactive
is used for creating reactive objects and arrays. It converts a plain JavaScript object into a reactive proxy. Any nested properties within this object or elements within the array also become reactive.
import { reactive } from 'vue'; // Creating a reactive object const user = reactive({ name: 'Alice', age: 30, address: { street: '123 Main St', city: 'Anytown' } }); console.log(user.name); // Alice // Modifying a property user.age++; console.log(user.age); // 31 // Modifying a nested property user.address.city = 'Newcity'; console.log(user.address.city); // Newcity // Example in a Vue component // <template> // <p>Name: {{ user.name }}</p> // <p>City: {{ user.address.city }}</p> // <button @click="user.age++">Grow Older</button> // </template> // <script setup> // import { reactive } from 'vue'; // const user = reactive({ // name: 'Bob', // age: 25, // address: { city: 'Oldcity' } // }); // </script>
reactive
uses JavaScript's Proxy
object under the hood. When you access or modify a property of a reactive object, the Proxy
intercepts these operations. This allows Vue to perform dependency tracking (when a property is read) and trigger updates (when a property is written).
The Relationship Between ref
and reactive
It's common to see ref
used even for objects within the setup
function when they are intended to be a standalone piece of reactive state. When a ref
holds an object, Vue automatically makes that object reactive using reactive
internally. So, ref({ data: 'value' })
is functionally similar to reactive({ data: 'value' })
, but the former still requires .value
access.
While reactive
directly exposes the reactive object, ref
provides a consistent interface (.value
) for both primitives and objects, which can simplify some patterns, especially when passing reactive values around.
The Orchestrator effect
At the core of how reactivity actually "does something" is the effect
function. It's not typically exposed directly for application development (unless you're building a library or deep diving into reactivity), but it's crucial to understand for the underlying mechanism. effect
takes a function as an argument, and this function is run immediately and re-run whenever any of the reactive dependencies accessed within it change.
Vue's rendering mechanism (template compilation) implicitly creates an effect
. When you define a component's <template>
, Vue compiles it into a render function. This render function becomes an effect
. When count.value
is read in the template, the render function registers count
as a dependency. When count.value
changes, the effect
(the render function) is re-executed, causing the component to re-render.
Let's illustrate a simplified version of how effect
facilitates this:
// This is a conceptual representation, not directly runnable Vue code // Imagine a simplified reactivity core let activeEffect = null; const targetMap = new WeakMap(); // Maps target objects to Maps of props to Sets of effects function track(target, key) { if (activeEffect) { let depsMap = targetMap.get(target); if (!depsMap) { targetMap.set(target, (depsMap = new Map())); } let dep = depsMap.get(key); if (!dep) { depsMap.set(key, (dep = new Set())); } dep.add(activeEffect); } } function trigger(target, key) { const depsMap = targetMap.get(target); if (!depsMap) return; const dep = depsMap.get(key); if (dep) { dep.forEach(effect => effect()); } } // Simplified ref implementation function ref(raw) { const r = { get value() { track(r, 'value'); return raw; }, set value(newVal) { raw = newVal; trigger(r, 'value'); } }; return r; } // Simplified reactive (Proxy) implementation function reactive(obj) { return new Proxy(obj, { get(target, key, receiver) { track(target, key); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver); trigger(target, key); return result; } }); } function effect(fn) { const effectFn = () => { activeEffect = effectFn; // Set the current active effect try { return fn(); // Execute the function, triggering tracks } finally { activeEffect = null; // Clear active effect } }; effectFn(); // Run immediately return effectFn; // Return for manual stopping (if needed) } // --- Usage Example --- const myCount = ref(0); const myUser = reactive({ name: 'Bob' }); effect(() => { console.log(`Count changed: ${myCount.value}`); }); effect(() => { console.log(`User name changed: ${myUser.name}`); }); myCount.value++; // Triggers "Count changed: 1" myUser.name = 'Alice'; // Triggers "User name changed: Alice"
In this simplified model:
- When
effect
is called,activeEffect
is set to the current function. - Inside the function, when
myCount.value
is accessed, itsget
handler callstrack
. track
usesactiveEffect
to register the currenteffect
function as a dependency formyCount
(specifically, its 'value' property).- Similarly, when
myUser.name
is accessed, itsget
handler (fromProxy
) callstrack
, registering theeffect
as a dependency formyUser
(specifically, its 'name' property). - When
myCount.value
is updated, itsset
handler callstrigger
. trigger
looks up all effects registered formyCount
's 'value' property and re-executes them.- The same logic applies to
myUser.name
when it's updated.
This constant interplay of track
and trigger
within ref
s and reactive
proxies, orchestrated by the effect
runner, forms the backbone of Vue's highly efficient and automatic reactivity system.
Conclusion
Vue 3's reactivity system, built upon ref
, reactive
, and the underlying effect
mechanism, offers a powerful and intuitive way to manage application state and ensure UI consistency. By leveraging ref
for primitive values and reactive
for objects and arrays, developers can declare reactive state that automatically triggers updates to any dependent effects, including component renders. This declarative approach vastly simplifies complex state management, making Vue applications more predictable and easier to maintain. The elegance of this system lies in its ability to automatically track dependencies and precisely localize updates, leading to highly optimized and performant user interfaces.