MDN URLSearchParams - discussion (2023-01-12)
I keep coming back to URLSearchParams when teams want to put *everything* into the URL and then discover the URL is not actually a state model. MDN explains the primitive well; I'm curious how people decide what becomes query params, what becomes route state, and what stays local document truth.
What do you keep in query params vs in route state (especially for filters and sort)? How do you avoid parsing/serializing params in five places and getting drift? Do you treat URL params as the source of truth, or as a hint that seeds route state once?
Comments (16)
Back to latestWe treat query params as a hint: read once, write route truth, then re-serialize for sharing.
If you treat the URL as the only truth, you end up with parsing code everywhere.
A small but useful convention: one module owns parsing/serialization and returns a typed object.
Then components only deal in the typed shape, not in strings.
We have a helper that normalizes params so you don't accidentally treat missing keys differently across panels:
ts
export type Filters = { q: string; sort: 'new' | 'top'; page: number };
export function parseFilters(search: string): Filters {
const p = new URLSearchParams(search);
const q = p.get('q') ?? '';
const sort = (p.get('sort') as Filters['sort']) ?? 'new';
const page = Number(p.get('page') ?? '1');
return { q, sort, page: Number.isFinite(page) ? page : 1 };
}Counterpoint: treating URL as hint can make deep links feel "soft" if route state diverges after initial seed.
We had to be careful to re-serialize route truth back into the URL so sharing still worked.
Yep, the hint approach only works if you have a consistent re-serialization step.
Otherwise you get the worst of both worlds: URL isn't truth, but also doesn't reflect truth.
For filters/sort, we keep them in route state because back/forward should replay them predictably.
The URL gets a short set of keys so sharing works, but the route contract is the real model.
We used to encode arrays in query params (e.g. tag=a&tag=b) and it got messy quickly.
Now we normalize arrays into a stable string so ordering doesn't cause cache differences.
MDN's doc is also a good reminder: URLSearchParams is not a router.
If you start using it as a router, you end up inventing navigation intent in string manipulation.
We render query/filters as evidence in the shell (data-q, data-sort, data-page).
If you do that, you can stop guessing whether the URL parse worked.
Docs tie-in: the routing posture in this repo is basically "URL gives where; state gives what".
Related: Routing and Navigation and The App Router Mindset.
We had a bug where two panels serialized sort differently (recent vs newest) and it broke sharing links.
Central parsing/serialization is worth it just to avoid that class of bug.
Counterpoint: if you don't put filters in the URL, users can't share the view they meant.
We ended up putting the keys users care about in the URL and keeping the rest route-only.
URLSearchParams is simple, but the design question isn't. The model matters more than the syntax.
One thing MDN doesn't cover: if you build cache keys from query strings, normalization is essential.
Different ordering of params can create surprising duplicate cache entries.
If you want to keep URL handling boring, write a module and never call new URLSearchParams() in components.
It's the easiest style rule we added and it paid off immediately.
We also store a derived shareUrl string as evidence so users can copy the exact canonical link for the current route posture.
It sounds redundant, but it prevents subtle drift between what's shown and what's shared.
This is a good example of why the forum patterns emphasize evidence.
If you can see the route truth in the DOM, URL parsing bugs aren't mysterious anymore.