Why can a single settle step leave a component test asserting on an intermediate render, and what loop fixes it?
answer
- settling can create more work
- an effect after commit writes state
- loop until nothing is pending
- cap the loop and report the cycle
basics
~20 sApplying one batch can create another: an effect that runs after commit writes state and queues a second update. So one settle can return with work still pending, and a harness typically loops — settle, check for pending work, settle again — until the tree is quiescent.
solid answer
~40 sA settle step drains the work that was queued when it started. Committing that work can queue more: an effect scheduled after commit writes derived state, a subscription delivers its first value the moment it is opened, a child reports something the parent stores. The result is a nested flush — the host now holds an intermediate render, one step short of what the component will eventually show. A harness therefore loops: settle, ask the framework whether work is pending, settle again, until nothing new is queued. The loop must be bounded, because a tree that queues fresh work on every commit has a genuine update cycle, and an unbounded harness hangs instead of reporting a real defect the application would also hit.
go deeper
Know the symptom: an assertion that sees neither the old value nor the expected one usually means the tree moved one step and stopped, and more settling is needed.
Explain where the second write comes from — a post-commit effect, a subscription emitting on open, a child reporting to its parent — and why a harness loops rather than settling once.
Show the diagnosis: settle in a bounded loop, watch whether output advances per pass, and separate a deep queue from an update cycle. Say what you did with the cycle when you found one.
Weigh a harness that loops to quiescence against one that settles once and forces tests to be explicit. Automatic looping hides real update cycles behind a slow-but-green suite unless the cap is low and loud.
## One flush, then another A settle step is not a promise that the tree is finished; it is a promise that **the work outstanding when it started** has been applied. If applying that work queues more, the test resumes with a host showing an intermediate state: the click was processed, the first render committed, and the follow-on update is still sitting in the queue. The symptom is recognisable once you have seen it. The assertion fails on a value that is not the old one *and* not the expected one — the tree moved one step and stopped. ## Where the second write comes from A nested flush is normal, not exotic. Common sources: - **An effect that runs after commit** and writes state — normalising an input, deriving a field from another, syncing two pieces of state that should have been one. - **A subscription that emits on open.** Subscribing during commit and receiving the current value immediately is a write inside the commit that caused the subscription. - **A child reporting upward.** A child notifies its parent during its own setup, the parent stores it, and the parent re-renders. - **A chain of derived state.** One derivation feeds another; each link is its own pass. - **A cascading conditional.** The first commit mounts a subtree whose own setup writes, so the second pass is the one that renders it. Frameworks differ in how visible this is. A runtime that re-runs the component function generally gives post-commit effects their own pass, so chains are common; a fine-grained runtime may propagate through its dependency graph before committing, collapsing several links into one host update; a compile-time runtime may schedule the follow-up as another commit you must await. Deeper queues are more likely in the first model and less likely in the second, but no model rules the second pass out. ## The loop a harness runs 1. Settle: drain the queued work. 2. Ask the framework whether anything is now pending. 3. If yes, settle again. 4. Repeat, counting the passes. 5. If the count exceeds a cap, stop and fail with that count. Step 5 is the part people leave out, and it is the part that matters. A tree whose commit always queues fresh work never becomes quiescent, and an unbounded loop simply hangs: the test times out with no information. With a cap, the failure names the condition — the tree never settled after N passes — which is a far better bug report, because it is a real defect. An application with that shape re-renders forever in production too; the test just found it first. ## Reading the symptom | What you see | Most likely cause | | --- | --- | | Old value after settling | nothing was queued — the write or the interaction never happened | | A value one step short of expected | a nested flush; settle again in a bounded loop | | Advances one step per extra pass | a chain of derived writes, deeper than the queue you drained | | Never quiesces, loop hits its cap | an update cycle in the component, not a harness problem | That table is the whole diagnostic. The distinction that matters is between *not enough settling* and *nothing to settle*: in the first, each extra pass moves the output; in the second, passes change nothing and the framework reports no pending work. ## Why not just settle a fixed number of times Because the depth is a property of the tree under test, not a constant. Two settles fix today's test and the third link added next month reintroduces the same failure somewhere else, with no signal about why. Looping to quiescence adapts to the tree; a magic number encodes one tree's shape into every test that copies it. The same argument rules out the habit of pasting extra settle calls until a test goes green — it hides the depth instead of measuring it. ## What this is not - It is not a fix for output that depends on work the framework never queued; a result still in flight elsewhere is a different problem with a different answer. - It is not a reason to distrust the settle primitive. The primitive is correct about the queue it drained; the test's assumption about queue depth was wrong. - It is not licence to loop forever. A bounded loop that reports its cap is the whole difference between finding a cycle and timing out. The mental model to carry: **settle until quiescent, with a cap** — and treat hitting the cap as a finding about the component, not about the harness.
- What should a harness do when the tree never becomes quiescent?Stop at a cap and fail loudly with the pass count instead of spinning. A tree that queues fresh work on every commit has an update cycle — typically an effect writing state that its own dependencies include — and the test has found a real defect. Hanging turns that finding into an uninformative timeout.
- How do you tell a missing second settle from output that was never queued?Settle in a bounded loop and look at the host after each pass. If the output advances one step per pass, the queue was simply deeper than the first flush drained. If nothing changes and the framework reports no pending work, the update never existed — the write or the interaction is the defect.
- Why is settling a fixed number of times worse than looping to quiescence?Because queue depth is a property of the tree, not a constant. A hard-coded count matches today's tree and silently breaks when another derived write is added, with no signal about why. Looping until the framework reports nothing pending adapts automatically and still terminates at its cap.
Wiping a counter while someone is still stacking dishes on it: one wipe clears what was there when you started. You keep wiping until nothing new appears — and if dishes never stop arriving, the problem is not your cloth.
saying these in an interview costs you the question
- Believes one settle step always leaves the tree quiescent
- Pastes extra settle calls in until the test goes green
- Treats a never-quiescing loop as a harness bug, not an update cycle
- Thinks an effect running after commit cannot queue more render work
- Assumes nested flushes only occur in runtimes that re-run component functions