Vue / Basic: reactive components and input contracts / Lesson 3
Reactivity, refs, and proxies
Understand which reads Vue tracks and which writes can trigger updates so state remains observable across boundaries.
On this page
Lesson goals
- Distinguish ref containers from reactive object proxies.
- Preserve reactive access when values cross functions or destructuring.
Track access rather than variable names
Vue’s reactivity connects an effect’s state reads to later changes in that state. A normal JavaScript variable is not automatically observable because its name appears in a template. ref wraps a value in a reactive container accessed through .value in JavaScript, while reactive returns a proxy for an object. Reading and writing through the intended container or proxy matters. A copied primitive is a value at one moment, not a live relationship. This model explains many apparent rendering bugs more precisely than saying Vue failed to notice a change. Identify the actual tracked read first.
Use ref consistently across script and template
The example owns one ref and exposes it through script setup. JavaScript increments count.value because it works with the container explicitly. The template unwraps a top-level ref in the documented template context, so it can display count without .value. Unwrapping rules are not universal: refs inside arrays and native collections can still require explicit value access. Keep the distinction visible when teaching helpers and data structures. A function receiving count.value gets the current primitive, while a function receiving count can retain access to the container and its future value.
<script setup>
import { ref } from "vue"
const count = ref(0)
function increment() {
count.value += 1
}
</script>
<template>
<button type="button" @click="increment">Count: {{ count }}</button>
</template>
In a configured Vue 3.5 SFC project, the button starts at Count: 0 and increments after activation. Script and template use different documented access forms. No Vue runtime or browser execution was performed for this fragment.
Retain the reactive access path
reactive deeply converts ordinary object access under its documented behavior. Destructuring a primitive property into a local variable disconnects that variable from later proxy access. Similarly, replacing a variable holding one reactive object with a new plain object does not make existing consumers follow automatically. Keep a stable reactive owner, use refs for replaceable values, or preserve property access through getters and appropriate ref utilities. Vue 3.5’s compiler-assisted destructuring of defineProps is a special SFC transform discussed later; it does not make arbitrary JavaScript destructuring reactive everywhere. Distinguish runtime behavior from compiler rewriting.
Prove the ordinary JavaScript copy boundary
The standalone example demonstrates a language-level fact that applies before any framework is involved: destructuring a number copies the current value. A getter function reads the object each time it is called. Vue can track such reads when they occur inside an appropriate reactive consumer, but this pure example does not prove tracking or render updates. Use it to separate a JavaScript misunderstanding from a Vue question. When designing a helper, state whether it accepts a snapshot, ref, getter, or reactive object, because those contracts imply different behavior after the source changes.
const record = { count: 1 }
const { count: snapshot } = record
const current = () => record.count
record.count = 2
if (snapshot !== 1 || current() !== 2) {
throw new Error("VALUE_COPY_MISMATCH")
}
This JavaScript uses no Vue dependency and can be checked under Node. The snapshot remains 1 and the getter returns 2. Framework observation, effects, and DOM updates are separate claims that are not tested here.
Expect batched rendering and verify the right boundary
Reactive state can change synchronously while DOM updates are buffered until Vue’s update cycle. Use nextTick when code genuinely needs the DOM after an update, rather than adding an arbitrary timeout or repeatedly forcing renders. Deep state, nested refs, and proxy identity deserve deliberate contracts when integrating external libraries. Avoid mutating an unproxied original object and expecting every consumer of a separate proxy path to behave as if the same access occurred. Begin diagnosis with owner, tracked source, mutation path, and observation timing. This provides a reproducible explanation before adding watchers that merely conceal disconnected state.
Common errors and fixes
A destructured primitive stops following updates
- Why it happens
- The consumer holds a copied value rather than reactive access.
- How to fix it
- Pass a ref or getter, or retain property access.
- Prevent it next time
- Document helper input contracts.
Script code changes the ref variable incorrectly
- Why it happens
- The container and its value are confused.
- How to fix it
- Read and update .value where the JavaScript API requires it.
- Prevent it next time
- Separate script rules from template unwrapping.
A DOM read happens before rendering completes
- Why it happens
- Synchronous state change is assumed to imply synchronous DOM update.
- How to fix it
- Use the documented update timing and nextTick when needed.
- Prevent it next time
- Test state and DOM boundaries separately.
Check your understanding
1. A helper receives count.value. Can it later observe the same ref automatically?
Show worked solution
Not from that primitive argument alone. It received a snapshot. Give the helper an explicit ref or getter contract when it must read future state, then use that access inside the appropriate reactive consumer.
2. Why does Vue 3.5 reactive props destructuring not fix all destructured object values?
Show worked solution
It is a compiler transform for defineProps bindings in a specific SFC context. Ordinary JavaScript destructuring still follows language semantics. Identify whether the compiler owns the expression before assuming a live reactive relationship.
Key takeaways
- Reactivity depends on tracked access and mutation paths.
- Refs and proxies have distinct contracts.
- A copied value is not a live source.