MDN URLSearchParams - discussion (2022-06-11)
For filter/search-heavy UIs: how do you handle query params without ending up with a bunch of sync effects and edge cases around back/forward?
Do you treat search params as route truth (and derive the rest), or treat them as a serialization format? How do you model multi-select params (repeated keys vs comma-separated) so it's stable and testable? And what signals do you render so bugs are understandable from a screenshot?
Comments (10)
Back to latestIf you have an effect that says "when params change, set state", you're already in trouble.
Make params the truth or make state the truth, but don't try to keep them in sync after render.
We treat params as serialized truth and store a normalized shape in the route doc as derived keys.
Raw params are wide/ugly. The doc holds a stable surface like { q, tags, sort } and everything renders from that.
This maps cleanly to Routing and Navigation and the docs posture in Core Concepts Best Practices.
Normalization snippet that avoids "mystery array vs string" bugs:
ts
type Filters = { q: string; tags: string[]; sort: 'new' | 'top' };
function parseFilters(sp: URLSearchParams): Filters {
const q = sp.get('q') ?? '';
const tags = sp.getAll('tag');
const sort = sp.get('sort') === 'top' ? 'top' : 'new';
return { q, tags, sort };
}Counterpoint: repeated keys make URLs annoying for some products (they insist on commas).
If you do commas, you need a single normalization helper and you can't let components split/join ad hoc.
Yep, product is already hinting at commas. I'm trying to keep it from becoming "everyone parses it differently".
Then I'd treat parsing as a route boundary and store the normalized tags array as the contract.
Signals are huge here. We render a tiny filterSignal={q,tagsCount,sort} in the shell.
It stops bug reports like "it filtered wrong" from being vague. You can see what the app thought the filters were.
We basically copied the render-visible contract signal idea from Testing and Debugging and it made reproductions so much easier.
Also: prefer explicit reset actions over effects that try to fix state when params disappear. Timing bugs are brutal here.
We made default params part of the contract: if sort isn't present, it's new and we render that as evidence.
Otherwise components treat "missing" differently and UI drifts between panels.
The failure mode for us was encoding too much derived stuff into params (like precomputed ids).
We only keep inputs in params. Everything derived is stored as evidence in the doc so it stays deterministic and testable.
If you can paste the URL into chat and someone can predict the UI from it, you did it right.
Query params are fine. The bug is letting parsing be distributed across components. Centralize parsing, store normalized contract, render signals.