How does an effect with a declared dependency list differ from one whose dependencies are tracked automatically, and what fails in each?
answer
- the runtime needs a dependency set
- declared promise versus recorded reads
- omitted value serves stale data
- tracking misses conditional and post-pause reads
- unstable dependency re-runs everything
basics
~20 sA declared list re-runs the effect when the listed values change, and silently serves stale data for any value omitted. Automatic tracking records the reactive values actually read during the run, and misses reads made conditionally or after an asynchronous pause.
solid answer
~50 sBoth models need the same thing - the set of values whose change should re-run the effect - and they obtain it differently. With a declared list the author writes the values down and the runtime compares them between updates, so the list is a promise about the body: omit a value and the effect keeps acting on the one it captured last, which shows up as stale behaviour rather than an error. With automatic tracking the effect runs inside a recording scope, every read of a reactive value subscribes it, and the set is rebuilt on each run - accurate for anything read plainly and synchronously, but blind to a read behind a branch that was not taken and to reads after the run resumes from an asynchronous pause. Both models share the over-broad failure: a dependency rebuilt on every update re-runs the effect constantly.
go deeper
Know that an effect re-runs when the values it depends on change, and that the runtime has to be told - or has to observe - which values those are. Recognise stale behaviour as a dependency problem.
Explain both mechanisms and their opposite failure modes: an omitted declaration gives wrong data with the right number of runs, while an unobserved read gives right data that never arrives.
Debug from the symptom. Decide whether an effect is stale, over-firing, or never firing, and know which dependency mistake produces each. Stabilise dependency identities rather than muting the tool.
Judge the model itself when choosing a runtime: a declared set is auditable but hand-maintained, a recorded set is accurate but invisible. Say which review and lint practices each one obliges your team to adopt.
## Two ways a runtime learns what an effect depends on An effect must re-run when something it uses changes, which means the runtime needs a **dependency set**. There are two ways to obtain one, and they fail in opposite directions. **A declared list.** The author writes down the values the effect uses. On each update the runtime compares the current values against the ones from the previous run - usually by identity, sometimes by a shallow comparison - and re-runs the effect only if one differs. The list is a *promise* the author makes about the body, and nothing forces the promise to be true. **Automatic tracking.** The runtime runs the effect inside a recording scope. Every read of a reactive value during that run registers the effect as a subscriber of that value. When one of those values is written, the effect is re-run and the set is recorded again from scratch. Nobody declares anything; the set is whatever the last run actually touched. ## Where a declared list fails - **Under-declared.** The body reads a value the list omits. In a runtime where each run captures the values of the update it belongs to, the effect keeps acting on the value from its last run - a *stale* one. The subscription is opened for the previous room, the timer computes against the previous interval, the saved payload is one edit behind. Nothing crashes, which is what makes it expensive. - **Over-declared.** The list contains something that compares unequal on every update - an object, array or function rebuilt each time the surrounding code runs. The effect re-runs constantly: the subscription is torn down and reopened per keystroke, and the external system sees churn instead of a stable connection. - **Declared but unused.** A value in the list the body no longer reads. Harmless to correctness and misleading to the next reader, who will infer a dependency that is not there. Because the list is a claim about the body, this model leans on tooling and on the discipline of re-reading the body against the list whenever either changes. ## Where automatic tracking fails Tracking cannot record a read it never observes. Three shapes escape it: 1. **A read behind a branch that was not taken.** If the first run skipped the branch, the value inside it was never read, so the effect is not subscribed to it. Change only that value and nothing re-runs. The effect becomes correct again only once something else re-runs it and the branch is taken. 2. **A read after an asynchronous pause.** The recording scope covers the synchronous part of the run. Once the effect yields and resumes later, reads usually fall outside the scope and register nothing - so the effect depends on what it read before the pause and not on what it read after. 3. **A read through a copy taken out of tracking.** If a value was already unwrapped into a plain local, or read in a scope that deliberately suppresses tracking, the read the runtime sees is of the plain copy, and writes to the original never reach the effect. And the over-broad failure has a mirror here too: an effect that reads a whole container when it needs one field subscribes to every change to that container. ## The two models side by side | | Declared list | Automatic tracking | |---|---|---| | Who determines the set | The author, explicitly | The runtime, from reads made during the run | | Classic bug | Omitted value, so the effect acts on stale data | Conditional or post-pause read, so it never re-runs | | How it surfaces | Silently wrong values | Silently missing re-runs | | Over-broad form | An unstable value in the list re-runs everything | Reading a whole container instead of one field | | Keeps the set honest | Tooling plus review of body against list | Read exactly what you depend on, synchronously | | Changing the body | Requires updating the list too | Set updates itself on the next run | ## Practical rules that hold in either model 1. **Read narrowly and read early.** Touch exactly the values the effect depends on, at the top of the run, before any pause. This makes the tracked set right and makes a declared list easy to verify. 2. **Keep dependency values stable.** If a dependency is rebuilt on every update, either derive it so it stays identical while its inputs do, or depend on the primitive parts you actually use. 3. **Split by trigger.** If half the body should re-run on one value and half on another, that is two effects with two dependency sets, not one effect with the union. 4. **Treat a stale read as a dependency bug first.** "It is using the old value" nearly always means the runtime was never told that value matters. ## Why the difference is worth explaining out loud The two models push the same defect to opposite ends. A declared list makes the set visible and puts accuracy on the author, so its failure is *wrong data with the right number of runs*. Automatic tracking is accurate by construction for anything read plainly and synchronously, so its failure is *right data that arrives late, or never*. Knowing which family you are in tells you which question to ask first.
- Why does an object rebuilt on every update re-run an effect that lists it, even when its fields are identical?Because the comparison between runs is by identity for reference values, and a freshly built object is a new identity every time. The runtime sees a changed dependency and re-runs. The fixes are to depend on the primitive fields the body actually reads, or to derive the object so its identity survives while its inputs do.
- If automatic tracking rebuilds the dependency set on every run, how can an effect ever end up subscribed to too much?By reading more than it needs. Touching a whole container to use one field subscribes the effect to every change in that container, so unrelated writes re-run it. Reading the specific field, or a derivation that narrows to it, shrinks the recorded set to what the effect genuinely depends on.
- An effect reads a value only inside a branch that was false on its first run. What happens when that value changes?Under automatic tracking, nothing: the read never happened, so no subscription exists and the write reaches no one. The effect corrects itself only when something else re-runs it and the branch is taken. Reading the value unconditionally at the top of the run, or making the branch condition itself part of the set, restores the dependency.
saying these in an interview costs you the question
- Thinks the dependency list controls only re-running, not which values the body sees
- Assumes automatic tracking cannot miss a dependency because it is automatic
- Believes reference dependencies are compared field by field
- Adds an omitted value to the list by suppressing the warning instead
- Treats a stale value as a rendering bug rather than a dependency bug