Reactivity is the part of Vue you have to actually understand. Everything else is syntax around it. The idea is simple: you declare state, you use it in a template, and when the state changes the template updates — without you telling it to.
The mechanism is dependency tracking. While a component renders, Vue records every reactive value that render read. When one of those values changes, Vue re-runs that render and nothing else. There is no dependency array to maintain and no equality function to pass, because Vue already knows exactly which renders touched which values.
ref
ref() wraps a value in a reactive container:
import { ref } from "vue";
const loading = ref(true);
const reels = ref([]);
const activeIndex = ref(0);In JavaScript you read and write through .value:
loading.value = false;
reels.value.push(...res.items);
if (activeIndex.value === 0) { /* ... */ }In a template you do not:
<LoadingSpinner v-if="loading" />
<p>{{ reels.length }} reels</p>That inconsistency is the single most common source of early confusion, so it is worth knowing
why it exists. .value is the only way to make a plain value reactive in JavaScript —
assigning to a bare variable is invisible to any library, because there is no hook to intercept it.
A property access is interceptable, so ref gives the value a property. Templates are
compiled, so the compiler knows which bindings are refs and unwraps them for you.
Forgetting .value
This is the bug you will write in your first week:
const loading = ref(true);
if (loading) { /* ALWAYS true -- an object is truthy */ }
if (loading.value) { /* correct */ }
loading = false; // TypeError: assignment to constant
loading.value = false; // correctThe const assignment throws, which is loud and easy. The truthiness one is silent
and will cost you an afternoon. If a condition that should sometimes be false never is, this is
why.
ref holds anything
Not just primitives. A ref containing an object or array is deeply
reactive — mutating something nested triggers updates:
const items = ref([]);
function push(message, variant = "success", ms = 3600) {
const id = nextId++;
items.value.push({ id, message, variant });
setTimeout(() => dismiss(id), ms);
return id;
}items.value.push(...) mutates the array in place and the toast list re-renders.
Vue wraps the array in a proxy, so the mutation is observable.
reactive
The other way to declare state. reactive() takes an object and returns a reactive
proxy of it, with no .value anywhere:
import { reactive } from "vue";
const filters = reactive({ status: "", creator: "", q: "" });
filters.status = "PUBLISHED"; // no .value
console.log(filters.q);That looks nicer, and it is why people reach for it. Then they hit the limitations.
Three ways reactive will surprise you
1. It only works on objects. reactive(0) and
reactive("hello") do nothing at all — there is no object to proxy. Primitives need
ref, so a codebase using reactive ends up using both anyway.
2. Destructuring breaks it. This is the big one:
const filters = reactive({ status: "", q: "" });
// `status` is now a plain string, disconnected from the proxy. It will never
// update again, and nothing warns you.
const { status } = filters;The same applies to passing filters.status into a function: you hand over a value,
not a reference to reactive state.
3. Reassignment breaks it. The reactivity belongs to the proxy object, so replacing the object throws it away:
let filters = reactive({ status: "" });
filters = { status: "PUBLISHED" }; // reactivity lost -- this is a plain object now
// With a ref, replacing the whole value is fine and is the normal thing to do:
const filtersRef = ref({ status: "" });
filtersRef.value = { status: "PUBLISHED" }; // still reactiveWhich to use
Use ref for everything unless you have a specific reason not to.
That is the rule this track follows and the rule the demo application follows — it contains 78
ref calls and not a single reactive.
The reasoning: ref works for every type, survives destructuring and reassignment, and
is what composables return anyway. The price is .value, which is a small, consistent,
visible tax. reactive removes that tax and replaces it with three failure modes that are
all silent.
Vue's own documentation now recommends ref as the default for the same reasons.
Related functions you will see
toRef(obj, 'key') creates a ref that stays linked to one property of a reactive
object, and toRefs(obj) does it for every property at once — which is how you
destructure a reactive without breaking it. If you follow the rule above you will rarely
need either, but you will read them in other people's code.
shallowRef makes only the .value assignment reactive, not anything
nested. It is a performance tool for large objects you replace wholesale rather than edit; lesson 25
covers when it is worth it.
readonly(obj) returns a proxy that warns on any write. It is the right way to hand
state to a child that should only read it, and it comes up again in lesson 16.
What reactivity does not do
It tracks reads during a render or an effect. Two consequences:
A value read outside any reactive context — in a plain module-level function, say — is not tracked, and changing it updates nothing.
An async function loses tracking after its first await. Anything read
after the await is not registered as a dependency. This matters for watchers, and lesson 7 comes back
to it.
Next: Computed Properties — derived state that caches itself.