A filter panel keeps its own copy of the filters and also writes them into the address; what goes wrong?
answer
- two copies, no arbiter
- Back moves the address, not the copy
- two-way sync oscillates or races
- derive downward, write by navigating
- an editing buffer is the legitimate local copy
basics
~20 sTwo copies of one value drift apart. Back and Forward change the address while the panel keeps its stale copy, and code that syncs both directions can loop. Make the address authoritative and derive the displayed values from it.
solid answer
~50 sDuplicating the state is the defect. Once the filters live in the panel *and* in the address, every path that changes one must remember to change the other, and the forgotten paths are the ones users exercise most: Back and Forward move the address without the panel noticing, and an in-app link that sets different parameters updates the address while the panel still shows its initial values. Patching that with an effect in each direction is worse - each write triggers the other, so you get a loop or a fight over which value is newer. The fix is one direction of derivation: **the address owns the state it carries**, the panel renders values derived from the current address, and a user change is expressed as a navigation. Local state remains only as an editing buffer, committed on debounce or submit.
go deeper
Learn the rule before the reasoning: if a value lives in the address, read it from the address instead of keeping a second copy beside it. Two copies of one value will disagree.
Explain the failure paths - history navigation and later address changes bypass a panel's own change handler - and why an effect in each direction loops instead of fixing it.
Show the design: address authoritative, one parse function, values derived downward, interactions expressed as navigations, identity writes as no-ops, and tests that drive the address rather than the panel.
Weigh what making the address authoritative commits the team to: a user-visible parameter vocabulary, parsing tolerant of anything pasted, and a convention that must survive many screens written by many people.
## Two copies of one value When a screen's filters are held in component state *and* written into the address, the application has two representations of one fact and no rule about which is right. Every mutation path must now update both, and paths that are not written by hand update nothing. The result is a class of defects that reproduce reliably for users and rarely for developers, because developers change filters by clicking the panel - the one path that does update both. ## The three symptoms 1. **Back and Forward move the address, not the screen.** The user narrows a filter, presses Back, watches the address revert, and the panel and the results stay as they were. History navigation changes the address without going through the panel's own change handler, so a copy written only by that handler never learns. 2. **A later address change is ignored.** The panel initialised its copy when it first appeared. Any subsequent change of address that does not remount it - an in-app link that sets different parameters, a programmatic navigation, a restored entry - leaves the copy at its initial value while the address says something else. 3. **A sync loop, or a fight over recency.** The usual patch is an effect that copies the address into the state and another that copies the state into the address. A write on either side now triggers the other, and unless both directions are exactly idempotent the pair oscillates, floods history with entries, or settles on whichever value happened to be applied last - non-deterministically, because it depends on scheduling. ## Why "just sync them" cannot be made safe A two-way sync maintains an invariant with two writers and no arbiter. To be correct it must guarantee that writing an identical value is a complete no-op on both sides, that no ordering of the two effects produces a different outcome, and that an interrupted or superseded navigation cannot resurrect an older value. That is a lot of care for a problem you can delete instead, by removing one of the two copies. ## The fix: one direction only - **Declare the address authoritative** for the state it carries: view, entity, filters, sort, page. - **Derive downward.** The screen reads the current address, parses it once into a typed view model, and renders from that. The panel's checkboxes and menus display derived values and hold no copy. - **Write by navigating.** A user interaction produces a new address, and the screen re-renders because the address changed. Exactly one writer, exactly one reader. - **Make the identity case a no-op.** Choosing the value that is already active should produce no change at all, which removes the oscillation risk by construction. - **Parse in one place.** One function from address to view model, used by the screen, the data layer and the tests, so no two consumers disagree about what a parameter means. Frameworks differ in how the re-render happens - a runtime that re-runs the component function re-reads the address on each pass, one with fine-grained tracking recomputes only the derived values that actually changed, and a compile-time runtime wires those dependencies during the build - but the shape of the fix is the same in all of them: a single source, read through, no mirrored copy. ## Where local state is still correct One-directional derivation does not mean every interaction writes the address immediately. | Situation | State | Commit into the address | |---|---|---| | Text being typed into a search box | local editing buffer | on debounce, or on submit | | A multi-step filter behind an Apply button | local draft | when the user applies it | | Toggling a single checkbox filter | none - derived | immediately, as the interaction | | Optimistic feedback while a navigation resolves | local pending value | dropped when the new address renders | The distinction is committed versus in-progress. In-progress input is legitimately local, because it is not yet a place the user is at. Once committed it belongs to the address, and the local buffer stops being authoritative. ## Testing it Drive the address, not the panel. Set an address and assert the panel shows the matching values; change the address to a different set and assert the panel updates without remounting; step back through history and assert the panel follows; choose the value that is already active and assert nothing changes. A screen that passes those cannot be holding a hidden second copy.
- If the address is authoritative, how does a search input stay responsive while typing?Keep the characters in a local editing buffer so each keystroke renders instantly, and commit that buffer into the address on a debounce or on submit. The buffer is in-progress input rather than a place the user is at, so it is not a duplicate of address state - and once committed, the address is what the screen renders from.
- By what mechanism does a two-way sync flood the history stack?Each direction treats the other's write as a change worth propagating. The address write updates the copy, the copy's change handler writes the address again, and unless writing an identical value is a strict no-op on both sides, every cycle produces another navigation. The user then needs many Back presses to escape one filter change.
- Does making the address authoritative cost anything?It concentrates the contract: parameter names and spellings become user-visible and hard to change, parsing must tolerate anything a pasted link contains, and interactions are expressed as navigations rather than local writes. That is a real cost, paid once, against defect classes that otherwise recur on every screen.
Two clocks in one room never agree for long. Hang one clock and let every other display read from it.
saying these in an interview costs you the question
- Mirroring address state into component state and syncing with an effect in each direction
- Reading the address only when the screen first appears, never afterwards
- Assuming history navigation goes through the panel's own change handler
- Resolving disagreements by preferring whichever copy changed most recently
- Writing the address for a value that is already active, creating redundant entries
- Parsing the same parameters separately in the panel and in the data layer