[Guide] Build a Search UI (Debounce Gates Everywhere) - implementation notes
The Search UI guide leans hard on "debounce gates" as the way to coordinate motion and data: typing is draft truth, gates decide when to commit, and route transitions happen when the gate fires—not on every keystroke. Pair that with render fetch results and stored-derived suggestions so list/detail panels stay coherent even when the network is noisy.
How did you structure debounce gates so they were predictable (and not just a delayed spinner)? Did you keep query in a route doc, a local draft, or both (multi-truth)? What evidence did you render so a screenshot tells you what the gate decided and why? How did you handle route transitions (results/detail) without letting stale queries leak into navigation?
Comments (24)
Back to latestDebounce gates felt sane once we stopped treating debounce as "a timer" and started treating it as "a decision".
If the gate has reasons (typing, paste, clear, submit), it stops feeling random.
We logged gate transitions like contract lines and it made debugging search UX 10x easier:
txt
[tips] gate=searchCommit allowed=false reason=typing remainingMs=220 qLen=2
[tips] gate=searchCommit allowed=true reason=debounce:fire q="re"
[tips] routeMotion results->results reason=gate:commit remountKey=query:re
We also render data-gate=typing|committed, data-query, and data-commit-reason on the shell.
Counterpoint: debounce gates can make fast typists feel like the UI is lagging behind them.
We shortened the window and added an explicit "Enter commits now" intent so the user can choose the boundary.
Yes. Gate posture shouldn't be one-size-fits-all.
We made the gate window part of posture (hot/warm) and logged the posture switch so it's explainable.
We kept query as multi-truth: local draft for typing, route doc for committed query.
If you commit every keystroke to route truth, back/forward becomes chaotic and logs become noise.
Stored-derived suggestions were worth it because multiple panels need the same suggestion contract (top suggestions, recent suggestions, spelling fixes).
If each panel computes suggestions, they drift and users think results are inconsistent.
We modeled suggestions as a lane too (pending/ok/empty/error) and we logged it like everything else:
txt
[tips] derive=suggestions lane=pending reason=gate:commit q="react"
[tips] derive=suggestions lane=ok reason=fetch:resolve count=8
It sounds like overkill until you debug an "empty suggestions" report from a screenshot.
Render fetch posture made tests and debugging simpler because the UI always has a lane for network work.
We stopped doing "fetch in effect then setState" and started treating the fetch lane as the truth.
Counterpoint: stored-derived suggestions can become a second cache and hide staleness bugs.
We included suggestionsDerivedAt and suggestionsSourceRevision as evidence so you can tell if you're looking at old output.
Long-form argument: a search UI is a coordination problem more than a data problem.
Without explicit gates, you get accidental coordination via timeouts, effect ordering, and "hope".
Gates give you a vocabulary for coordination, and logs/evidence make that vocabulary enforceable.
Docs tie-in: the data fetching posture and API reference pages are basically prerequisites for this style.
Related: Data Fetching and Caching (Best Practices) and API Reference.
We also logged and rendered route transitions as a first-class surface so stale queries couldn't "teleport" into navigation:
txt
[tips] routeMotion results->detail reason=row:click queryKey=query:react selectedId=doc_19A practical trick: treat paste as an immediate commit boundary (no debounce). Users paste expecting a result now, and the log reason makes it auditable:
txt
[tips] gate=searchCommit allowed=true reason=paste:commit qLen=24Counterpoint: too much gate logic can make search feel like it's fighting the user.
We kept the gate rules minimal: debounce on typing, commit on enter/paste/clear, and that's it.
We rendered the gate status in plain text in a visually-hidden element so tests can query it without attribute selectors.
It also helped screen readers because they can announce "search updating" in a controlled way.
Long-form counterpoint: debounce gates are a substitute for explicit product decisions about latency.
If your backend is slow, gates can only hide it. You still need a UX posture for stale vs fresh and a clear lane story.
The thing I like about the guide is it forces you to explain search behavior in sentences.
If you can't write a log line like [tips] gate=searchCommit allowed=true reason=..., the behavior probably isn't coherent yet.
We used a derived queryKey string so every panel can talk about the same identity for a search.
It made caching/merging decisions easier because you can log winners in one vocabulary.
We added a "cool mode" gate posture for low-power devices (longer debounce window + fewer derives).
The key was making the posture visible and reversible, not automatic and mysterious.
Search UIs can get into weird states when you have list + detail + overlay all reading different pieces of state.
The route doc + derived suggestion bundle prevented that for us because everyone reads the same committed query and the same outputs.
If you want this to feel professional, invest in the "why" language.
We store lastCommitReason and display it in debug mode. It sounds small but it prevents a lot of arguments.
Testing story improved once we could assert on gate + lane transitions rather than timers.
Gates are basically timing made explicit.
We also added a "results revision" evidence key so list and detail can detect if they disagree:
txt
[tips] evidence resultsRevision=128 queryKey=query:reactCounterpoint: it’s easy to cargo-cult debounce gates and end up with a pile of ad-hoc rules.
The guide works when the rules are few, named, and aligned with user intent boundaries.
If you're implementing this, start by rendering the evidence keys first: queryKey, gate posture, fetch lane, suggestions lane.
Once those are visible, you can iterate on the UX without losing track of what's actually happening.