Reactivity for Real
Phase 1 promised that Vue's data "knows when it's read and written." This phase is how - because the day reactivity silently stops working (and that day comes for everyone), the difference between a five-minute fix and a lost evening is knowing what the tracking system physically is.
The mechanism under everything
Vue's reactivity is built on JavaScript proxies - objects that wrap your data and intercept
every property access. Read state.count and the proxy's get trap fires: "the thing currently
rendering just read count - subscribe it." Write state.count = 5 and the set trap fires:
"notify everyone subscribed to count." That's the whole magic: interception at the property
level.
Hold that picture, because both of this phase's traps are cases where the interception gets bypassed.
ref: any value, one container
import from 'vue';
const count = ;
const user = ;
console.log; // 0 - in script, the value lives on .value
count.++; // write through .value = tracked
user.. = 'Grace'; // nested mutation is tracked too
What it actually is. A ref is a container object with a single reactive property: .value.
Why a container at all? Because JavaScript can't intercept a plain variable - let count = 0 has
no property to trap. Wrapping the value in an object gives the proxy machinery something to hold
onto. .value is not ceremony; it is the reactive access point.
In templates, .value disappears. Vue auto-unwraps top-level refs in templates:
{{ count }}, not {{ count.value }}. Convenient, and the source of the most common beginner
error - in the script, forgetting .value:
const count = ;
count = count + 1; // ✗ TypeError: Assignment to constant variable (best case)
count. = count. + 1; // ✓
Best case, const saves you with a loud error. Worst case (with let), you've replaced the
container with a plain number, and reactivity is simply gone - no error, no updates, ever again.
reactive: proxy an object in place
import from 'vue';
const form = ;
form.++; // no .value - the object itself is the proxy
reactive wraps an object (only objects - not numbers or strings) so its properties are tracked
directly, no .value anywhere. Reads naturally, mutates naturally. So why doesn't everyone use it
for everything? Because of the biggest trap in Vue:
The destructuring trap
const form = ;
const = form; // ✗ name is now a plain, dead string
let email = form.; // ✗ same problem
// later...
name = 'Ada'; // updates nothing, tracked by nothing
Why this breaks. Destructuring copies the value out of the proxy. The copy is an ordinary
string with no connection to the tracking system - reading it registers nothing, and you can't even
write back through it. The proxy only intercepts access through itself (form.name); the moment
a primitive leaves the proxy, it's inert. The same applies to passing form.name into a function,
or spreading: { ...form } produces a completely non-reactive object.
The fixes, in order of preference:
// 1. Just keep the object together: form.name everywhere. Simplest, usually right.
// 2. Need to hand pieces around? toRefs converts each property into a linked ref:
import from 'vue';
const = ; // ✓ refs, connected to form
name. = 'Ada'; // updates form.name, tracked
💡 Key point: this trap is why many Vue teams standardize on ref for everything, objects
included. Refs survive being passed around as the container - you hand over the box, not the
contents, and access through .value always goes through the tracking. The practical rule: ref
by default; reactive for a group of fields you'll always keep together and never destructure.
Both are correct Vue; the rule optimizes for the mistake humans actually make.
computed: derived values with a memory
import from 'vue';
const todos = ;
const remaining = ;
What it actually is. A read-only ref whose value is computed by your function - with two properties a plain function call doesn't have:
- It's reactive both directions. The computation reads
todos.value, soremainingsubscribes totodos; anything readingremainingsubscribes to it. Change a todo and the chain re-evaluates: data → computed → template. - It caches. The function re-runs only when a dependency changed. Ten template references to
{{ remaining }}cost one computation. A method called from the template runs every render.
The smell it exists to fix: state that duplicates other state. If you're maintaining a
remainingCount ref by hand next to todos, you've created two sources of truth that will
disagree someday. If it can be computed, computed it.
⚠️ Gotcha: computeds are for deriving, not doing. No mutations, no fetches, no
Math.random() inside - the function may run at unpredictable times (or not at all, if cached), so
side effects in a computed produce heisenbugs. Side effects belong in handlers and watchers
(phase 6).
Recap
- Reactivity = proxies intercepting property access: reads subscribe, writes notify.
refwraps any value; script access is.value, templates auto-unwrap. The container is the reactive unit - pass the box, not the contents.reactiveproxies objects in place - and destructuring/spreading copies dead values out.toRefswhen pieces must travel.- Default to
ref; usereactivefor keep-together field groups. computed= cached, chainable derivation. Derive, don't duplicate - and no side effects inside.
[
{
"q": "const { name } = reactive({ name: 'Ada' }) - and updates to name no longer affect the UI. Why?",
"choices": [
"reactive only works on nested objects, not strings",
"Destructuring copied a plain value out of the proxy - access no longer goes through the tracking system",
"The template needs {{ name.value }} to unwrap it",
"reactive objects are read-only outside the component that created them"
],
"answer": 1,
"why": [
"reactive tracks properties of any type - through the proxy; the string was fine until it was copied out.",
null,
"There's no ref here to unwrap - the copy is a dead primitive, and no syntax revives it.",
"reactive objects are freely writable anywhere - through the proxy."
],
"explain": "The proxy can only intercept access through itself. Destructuring hands you the contents without the container - use form.name directly, or toRefs(form) to get linked refs."
},
{
"q": "When does a computed re-run its function?",
"choices": [
"On every template render that references it",
"Only when one of the reactive values it read last time has changed",
"On a timer that Vue manages",
"Every time any state anywhere in the component changes"
],
"answer": 1,
"why": [
"That describes a method call in a template - the exact cost computed's cache avoids.",
null,
"Nothing is polled; the dependency graph triggers re-evaluation.",
"Only its own dependencies matter - unrelated state changes don't touch it."
],
"explain": "A computed tracks what it reads and caches its result. Its dependencies changing invalidates the cache; the next read re-computes. That's why it beats a method for anything referenced repeatedly."
},
{
"q": "Why does ref exist at all - why can't Vue just track let count = 0?",
"choices": [
"It could, but ref makes the code more readable",
"JavaScript offers no way to intercept reads and writes of a plain local variable - only property access on an object can be trapped",
"Refs are needed for TypeScript support",
"Plain variables would be too slow to track"
],
"answer": 1,
"why": [
"It's a language constraint, not a style choice - there is no API for observing a local binding.",
null,
"TypeScript types refs nicely, but the container exists for the proxy machinery, not the type system.",
"Speed isn't the issue - there's simply no interception point on a bare variable."
],
"explain": "Proxies trap property access on objects. A bare variable has no properties to trap, so Vue wraps the value in a one-property container: .value is the interception point."
}
]
← Phase 2: Templates That React · Guide overview · Phase 4: Components: Props, Events, and v-model →
Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.
Check your understanding 3 questions
1. const { name } = reactive({ name: 'Ada' }) - and updates to name no longer affect the UI. Why?
2. When does a computed re-run its function?
3. Why does ref exist at all - why can't Vue just track let count = 0?