Watchers, Lifecycle, and Fetching
Computed handles "this value follows from that value." But some reactions to change aren't values -
they're actions: refetch when the selected id changes, save a draft when the form changes, start a
timer when the component appears and stop it when it leaves. Actions in response to change are
watchers; actions tied to a component's existence are lifecycle hooks. Together they're
Vue's side-effect toolkit - powerful, and the part of Vue most often used where computed should
have been.
watch: when this changes, do that
import from 'vue';
const selectedId = ;
const product = ;
;
What just happened: watch subscribes to a reactive source (here a ref) and runs the callback on
change, with new and old values. The classic use is exactly this: a side effect (network call) in
response to a data change (selection).
The source argument has a shape rule that bites everyone once:
// ✓ a ref: watch the container
// ✓ a getter: watch an expression/property
// ✗ passes today's VALUE (a number) - nothing to subscribe to
Passing props.userId evaluates immediately to a plain number - dead, per phase 3, and Vue warns
about an invalid watch source. Anything that isn't itself a ref/reactive object gets wrapped in a
getter function.
Two options cover most real needs:
; // also run once right now (initial fetch + refetch in one)
; // fire on nested mutations inside an object
deep exists because watching a reactive object or a ref-of-object only fires by default when
the identity changes or (for reactive) properties are directly touched by the watcher's
tracking - mutating form.address.city deep inside won't trigger a shallow watcher on a ref.
deep: true traverses everything (at a cost proportional to the object's size); often the sharper
tool is watching the specific field: watch(() => form.address.city, ...).
📝 Terminology: watchEffect(fn) is the auto-tracked sibling: it runs immediately and re-runs
whenever anything it read changes - no explicit source list. Convenient for "keep these things in
sync" effects; less explicit about when it fires. Reach for watch when you care about which
change triggers the action (and want old/new values); watchEffect for fire-and-forget syncing.
Don't watch what you can compute
The most common watcher in beginner Vue is a hand-rolled computed:
// ✗ a second source of truth, updated by machinery
const fullName = ;
;
// ✓ a derivation
const fullName = ;
💡 Key point: if the reaction to data changing is producing another value, that's computed.
If it's doing something - fetching, saving, logging, touching a browser API - that's watch. The
computed version can't be stale, can't fire in the wrong order, and deletes three lines.
Lifecycle: onMounted and onUnmounted
A component's script runs before any DOM exists. Code that needs the real page - measuring an element, starting an interval, third-party libraries attaching to a node - waits for mount:
What just happened: onMounted fired after the component's DOM landed on the page;
onUnmounted fired when it left (a v-if went false, the route changed). The pairing is the
discipline: an interval, listener, or subscription started at mount and not stopped at unmount
keeps running against a dead component - the classic leak, stacking a new timer on every remount.
The full hook family exists (onUpdated, onBeforeMount, ...) but these two carry nearly all
real usage. If you're reaching for onUpdated, first ask whether a watch on the specific data
says what you mean more precisely.
The standard fetch shapes
Initial load - await inside onMounted (or just call an async function from setup):
Couldn't load orders.
Loading…
{{ o.ref }}
Refetch-on-change - the watch with immediate from above, which handles both first load and
every change of the source. And when the same shape repeats across components, phase 5 already
showed the endgame: fold it into a useFetch composable.
⚠️ Gotcha: the refetch watcher has the same stale-response race every framework meets: the
user flips from product 1 to product 2, responses arrive out of order, and 1 overwrites 2. The
watcher callback receives a third argument, onCleanup, made for exactly this:
;
Recap
watch(source, cb)= side effects on specific changes; sources are refs or getters, neverprops.xbare.immediatefor run-now-and-on-change;deep(or better: a targeted getter) for nested mutations;watchEffectwhen auto-tracking reads is clearer.- Producing a value →
computed; doing a thing →watch. Never maintain derived state by watcher. onMountedfor DOM-dependent setup;onUnmountedmirrors it - every start gets a stop.- Refetch watchers need
onCleanupfor the out-of-order response race.
[
{
"q": "watch(props.userId, cb) warns about an invalid source and never fires. Why?",
"choices": [
"Props can't be watched, only refs can",
"The expression evaluated to a plain number immediately - watch needs a ref or a getter like () => props.userId",
"The callback must be async",
"Watching props requires deep: true"
],
"answer": 1,
"why": [
"Props watch fine - through a getter that defers the read.",
null,
"Sync callbacks are fine; the source, not the callback, is broken.",
"deep governs nested objects - a primitive prop needs deferral, not depth."
],
"explain": "Arguments evaluate before the call: props.userId is already just 7 by the time watch sees it. A getter hands watch the recipe instead of the result."
},
{
"q": "A component starts a setInterval in onMounted with no onUnmounted. The component sits behind a v-if that toggles often. What accumulates?",
"choices": [
"Nothing - Vue clears intervals when components unmount",
"A new live interval per mount, each still firing against unmounted component state",
"Memory only, but the intervals themselves stop",
"Duplicate DOM nodes"
],
"answer": 1,
"why": [
"Vue tears down its own subscriptions - browser timers are yours to clear.",
null,
"The intervals keep firing forever - that's the leak's active half, not just retained memory.",
"The DOM is correctly removed; the timer outliving it is the problem."
],
"explain": "Whatever mount starts, unmount must stop. Each v-if cycle mounts a fresh component that starts a fresh interval; without clearInterval in onUnmounted they all keep running."
},
{
"q": "You need cartTotal to always equal the sum of cart item prices. Which tool?",
"choices": [
"A watcher on the cart that updates a cartTotal ref",
"A computed that reduces over the cart",
"onUpdated recalculating the total after each render",
"watchEffect writing into a cartTotal ref"
],
"answer": 1,
"why": [
"It works until the day an update path skips it - a maintained copy of derivable data is the anti-pattern this phase names.",
null,
"onUpdated fires after every render of anything - maximum wasted work, minimum precision.",
"Same second-source-of-truth problem in auto-tracking clothes."
],
"explain": "A value that follows from other values is a derivation: computed. Watchers are for actions - fetching, saving, timing - not for maintaining data that computed can express."
}
]
← Phase 5: Slots and Composition · Guide overview · Phase 7: When Vue Breaks →
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. watch(props.userId, cb) warns about an invalid source and never fires. Why?
2. A component starts a setInterval in onMounted with no onUnmounted. The component sits behind a v-if that toggles often. What accumulates?
3. You need cartTotal to always equal the sum of cart item prices. Which tool?