skip to content

In a framework that tracks reads and writes through a state handle, why do destructured copies stop updating?

level: middleimportance: should knowfreq 62%

answer

  1. tracking is on the handle
  2. a copy leaves the channel
  3. read at the point of use
  4. reads recorded only inside a tracked scope
  5. one consumer wrong, not all

basics

~20 s

Tracking lives on the handle, not on the value. A value copied out is a plain snapshot with no link to the handle, so later writes never reach it and nothing re-runs. Read through the handle at each use.

solid answer

~50 s

In a tracking runtime the state is reached through a **handle** that intercepts reads — a read registers a dependency — and intercepts writes — a write notifies whatever registered. Destructuring, spreading or otherwise copying a value out of that handle produces a plain value: it holds whatever was there at that instant, it cannot be notified, and the code holding it registered nothing, so a later write neither updates the copy nor re-runs that code. The symptom is telling — the value is right once and then frozen, while another part of the screen reading the same state through the handle keeps updating. So keep the handle as the thing you pass around, read at the point of use inside a tracked scope, and where plain code needs the value, hand it a function that reads it.

go deeper

for a junior

Remember where the magic lives: on the handle you read through, not in the value you get back. A copy you take out is an ordinary value and will never change on its own.

for a middle

Explain both escapes — a value copied out of the handle, and a read that happens when no tracked computation is running — and why each fails quietly in a different way.

for a senior

Use the fingerprint: one consumer frozen while others update points at how that consumer got its value. Watch asynchronous code, where the read after resuming is the one that counts.

for a principal

Decide the boundary contract for the codebase: what crosses as a handle and what crosses as a snapshot, so a plain value is understood as a snapshot by convention rather than mistaken for live state.

## What the handle is Some runtimes do not hand you a value at all; they hand you a **handle** to it. The handle is a channel with two directions: - **reads** through it are recorded — whatever computation is running at that moment becomes a dependent of that piece of state; - **writes** through it are announced — every recorded dependent is scheduled to run again. Because the write is observed as it happens, this model needs no before/after snapshot comparison, and mutating syntax is the supported way to update. That is a genuine difference from a snapshot model, not a stylistic one. But it comes with its own visibility rule, and the rule is about the channel: **a change is visible when it travels through the handle, and a dependency exists when the read travelled through the handle inside a tracked scope.** ## Two ways to leave the channel 1. **Copying the value out.** Destructuring a field into a local, spreading the state into a plain object, or assigning a property to a variable all produce something outside the channel. The local holds the value as of that moment. A later write goes to the tracked property, which the local has no relationship with, so the local keeps its old contents — and since nothing was registered, nothing re-runs to re-read it. 2. **Reading outside a tracked scope.** Tracking is scoped: reads are recorded only while a tracked computation is running. A read in a one-time setup path, inside a callback that fires later, or after an awaited step has resumed, happens when no computation is being recorded, so it registers no dependency. The value read is current, but it is the last value that code will ever see. The difference matters when debugging, because the two produce slightly different fingerprints: a copied value is stale from the moment of copying; an untracked read is correct at the moment it runs and then never refreshed. ## The fingerprint The distinctive symptom of leaving the channel is **disagreement between two consumers of one piece of state**. One region of the screen updates, another shows the first value forever. When all consumers are wrong together, suspect the write side; when one is wrong, suspect how that one obtained its value. ## What to do instead - pass the handle, or a small function that reads it, rather than the value; - read at the point of use, inside the scope that should re-run, not once at the top; - inside asynchronous code, read again after resuming rather than relying on a value captured before the pause; - write by assigning through the handle, never to a copy you took out of it; - when a plain value must cross a boundary — for example into code that knows nothing about the runtime — accept that it is a snapshot and arrange for that code to be called again, rather than expecting the value to change under it. ## Primitives, objects, and how far the tracking reaches Whether a value read out of a handle stays tracked depends on what it is and on how far the wrapping extends: - a **primitive** read out is a copy of a value, and can never be notified; - an **object** read out is the same object, and where the runtime's wrapping extends into nested objects, that nested object may itself still be a tracked handle, so writes into it are still observed — which is why teams sometimes find the trap bites for one field and not another; - a **copy you built yourself** from the handle's contents is plain, whatever the depth. That unevenness is the reason the discipline is stated as a rule about the channel rather than about data types: if you cannot say for certain that a value came through the handle at the point of use, assume it did not. ## The same rule in each model | model | how you make a change | the trap that hides it | |---|---|---| | snapshot comparison | build a new value and hand it back | writing into the value the runtime already held | | handle interception | assign through the handle | copying the value out, or reading outside a tracked scope | | compile-time instrumentation | assign to the declared reactive value | a write the compiler did not analyse, such as one through an alias | A codebase can contain more than one of these at once — a tracked container behind one boundary, plain snapshots handed across another — which is exactly how habits get carried into the wrong half. The portable question is not "may I mutate?" but "through which channel does this change travel, and did my read go through it?"

  • How is a copied-out value different from a read outside a tracked scope?
    A copied value is a snapshot taken at copy time and stale from then on. An untracked read returns the current value but registers no dependency, so the code around it is never re-run and keeps showing whatever it last read. Both fail silently; only the second is momentarily correct.
  • Why does passing a function that reads the state fix it?
    Because the read then happens where and when the consumer runs, inside that consumer's tracked scope. The dependency is registered on behalf of the code that needs to re-run, rather than on behalf of whoever happened to hold the handle first.
  • Is mutating state a mistake in a handle-based runtime?
    No — an assignment through the handle is the supported update, because interception makes the write itself the signal. The mistake is carrying that habit into a snapshot-comparing seam, or writing to a plain copy taken out of the handle, where no interception can observe it.

saying these in an interview costs you the question

  • Thinks a destructured local stays linked to the tracked property.
  • Says tracking is a property of the data rather than of the read.
  • Reads state once at setup and expects it to refresh later.
  • Writes to a plain copy and blames the runtime for missing it.
  • Assumes mutation is always wrong, regardless of the runtime's model.