A component has one effect that syncs the document title, a listener, a timer and a saved draft on every update - what is wrong?
answer
- one effect, one dependency set
- the union re-runs every bundled job
- a restarted timer never reaches its deadline
- split by trigger and lifetime
- some jobs were never effects at all
basics
~20 sBundling four concerns gives them one dependency set - the union - so any single change re-runs all four, restarting the timer and re-attaching the listener, and one cleanup block that must undo every combination. Split by trigger, one effect per concern.
solid answer
~50 sThe effect has the union of four dependency sets and a single cleanup for four jobs, so it re-runs all of them whenever any one input changes. A keystroke that only touches the draft rewrites the title, detaches and reattaches the listener and restarts the timer - which means a timer measured from setup may never reach its deadline while the user is active. The cleanup has to be correct for every combination the body may have taken, and nobody reading it can tell which value drives which job. Split by trigger and lifetime: one effect per concern, each depending on the narrowest values it reads, each with its own cleanup. While splitting, throw out what was never an effect - values that should be derived, work that belongs in the handler for the user action that caused it, and inputs copied into local state.
go deeper
Take away the habit: one effect per thing being synchronized. If a block does two unrelated jobs, it will run both when only one of them needed to.
Explain the mechanism - a shared dependency set is the union, so every input re-runs every job - and name the concrete damage, especially a timer that restarts before it can fire.
Lead the restructure: narrow each dependency set, pair each setup with its own cleanup, and remove the jobs that were derivations or handler work masquerading as effects.
Make the two questions a review standard: what change should cause this, and what must be undone before it happens again. A job without both answers does not belong in an effect.
## What a lump effect actually costs One effect containing four unrelated jobs - write the document title, attach a listener to a global object, keep a timer running, persist a draft - has one dependency set: the **union** of what all four jobs need. And it has one cleanup: a block that must undo all four. From that single fact the whole failure mode follows. - **Every job runs whenever any input changes.** A keystroke that only affects the draft rewrites the title, detaches and reattaches the listener, and restarts the timer. - **The timer never gets to run.** Restarted on each update, an interval or a debounce measured from setup effectively resets; work scheduled to happen "in two seconds" may never happen at all while the user types. - **The listener churns.** Detach and reattach per update is wasted work, and any state the external system holds about that registration - position, accumulated buffer, a one-shot flag - resets with it. - **Cleanup becomes guesswork.** The block must be correct for every combination of jobs that actually ran. If a branch inside the body skipped one of them, the cleanup has to know that too. - **Nobody can tell which value drives which job.** The union hides the real dependency graph, so the next reader cannot change any of the four safely. ## The restructure Split by **trigger and lifetime**, not by lifecycle phase. One effect per concern, each with the narrowest dependency set it genuinely needs and its own cleanup. | Concern | Its own trigger | Its own cleanup | |---|---|---| | Document title | The title text only | Restore the previous title if something else owns it | | Global listener | The identity of what it listens on | Remove that listener | | Timer | The interval and whether it should run at all | Cancel that timer | | Persist a draft | The draft value, usually rate-limited | Flush or cancel the pending write | Each row is independently reviewable, and the cleanup on each row is one line whose correctness is obvious. The cost of splitting is more effect declarations; the benefit is that a change to one concern cannot re-trigger the other three. ## The steps I would take 1. **List the jobs and the value each one reacts to.** If two jobs react to the same value *and* have the same lifetime, they may legitimately share an effect. 2. **Give each job the narrowest dependencies.** Depend on the primitive fields read, not on a container or a freshly-built object that changes identity every update. 3. **Pair setup with cleanup per job.** After the split, each cleanup closes exactly what its own setup opened. 4. **Delete the jobs that were never effects.** This is usually the biggest win, and it is the next section. 5. **Re-check the rate-limited ones.** Anything debounced or throttled needs its own effect, because it is the job most damaged by an unrelated re-run. ## The jobs that should leave entirely A lump effect is usually a collection point for work that had nowhere else to go. Before splitting, take out: - **Derived values.** Anything computed *for the UI* from state belongs where the output is described, or as a declared derivation - not written into state from an effect, which costs an extra update and can go stale. - **Work caused by a user action.** If it should happen because the user clicked, put it in that handler. It runs once, it reads the intent directly, and it does not have to be reverse-engineered from a state change. Deriving the action from the resulting state also mis-fires when the same state arrives another way. - **Copies of inputs into local state.** Two sources of truth for one value, kept in step by an effect, is a bug waiting for the third writer. - **Logging tied to a render.** Log where the decision is made, not where the state happens to settle. ## How to talk about the judgment The honest framing is that effects are the one place a component reaches outside itself, so they attract everything awkward. The discipline that survives review is a pair of questions per job: *what value changing should cause this, and what exactly must be undone before it happens again?* A job with a clear answer to both is an effect and should be its own. A job whose first answer is "a click" belongs in a handler; a job whose second answer is "nothing" is usually a derivation. Applying those two questions to the four-job effect above typically leaves two real effects, one handler, and one derivation - which is why "split it" understates the fix. The point is not tidiness; it is that each remaining effect now has a dependency set small enough that its re-run behaviour is predictable from reading it.
- Which of the four jobs is damaged worst by the bundling, and why?The timer. A listener re-attached per update wastes work but still functions, and the title is simply rewritten with the same text. A timer or debounce measured from setup restarts on every unrelated re-run, so under continuous typing its deadline is pushed forever and the work it guards may never run at all.
- When is it legitimate for two jobs to share one effect?When they react to the same value and have the same lifetime, and their cleanups are naturally paired - for example opening a connection and attaching its handler. Then the union is not a compromise, the cleanup has one obvious shape, and splitting would create two effects that must be kept in step by hand.
saying these in an interview costs you the question
- Organises effects by lifecycle phase rather than by concern
- Thinks one effect per component is tidier than several small ones
- Adds unrelated values to a dependency set to stop a stale warning
- Writes state from an effect for a value that could be derived
- Reacts to a click by watching the state the click changed