React Forum
Blog & Guide Responses
Sep 17, 2022 · 04:00 AM

MDN URLSearchParams - discussion (2022-09-17)

Started by Morgan Patel
externalmdnurlsearch paramsroutingfilters

For filter/search-heavy UIs: we keep oscillating between “query params are the truth” and “query params are just a serialization format” and it's showing up as weird back/forward behavior.

Read the docs

Do you treat search params as route truth (and derive everything else), or keep a route doc shape as the real contract? How do you model multi-select params (repeated keys vs comma-separated) so it stays stable across refactors? What do you render in the UI so support can understand the current filter posture from a screenshot?

Comments (10)

Back to latest
Asha Rahman
Sep 17, 2022 · 04:06 AM

Pick one truth. The bugs start when you “sync” after render.

Dmitri Kline
Sep 17, 2022 · 04:14 AM

We treat params as a transport and the route doc as the contract shape.

The URL is a hint; the doc stores a normalized { q, tags, sort } so the component tree reads one stable surface.

Related: Routing and Navigation and the “URL as hint” posture in Core Concepts Best Practices.

Salma Qureshi
Sep 17, 2022 · 04:22 AM

A small normalization layer goes a long way. We parse wide params into a narrow contract and render it:

ts
export function parseFilters(params: URLSearchParams) {
  const q = params.get('q') ?? '';
  const tags = params.getAll('tag');
  const sort = params.get('sort') ?? 'relevance';
  return { q, tags, sort } as const;
}

export function writeFilters(params: URLSearchParams, next: { q: string; tags: string[]; sort: string }) {
  params.set('q', next.q);
  params.delete('tag');
  next.tags.forEach((t) => params.append('tag', t));
  params.set('sort', next.sort);
}
Keira Santos
Sep 17, 2022 · 04:31 AM

Counterpoint: repeated keys (tag=a&tag=b) are stable but they’re annoying to diff and copy/paste for non-technical users.

We used comma-separated tags for a while because product wanted “pretty URLs”, then spent months handling escaping edge cases.

Morgan Patel
Sep 17, 2022 · 04:36 AM

Yeah, pretty URLs are seductive.

The repeated-key approach is the only one where we never had to explain “why did this tag disappear on back/forward”.

Keira Santos
Sep 17, 2022 · 04:39 AM

Agreed. If it’s a contract, choose stability over aesthetics.

Nico Alvarez
Sep 17, 2022 · 04:45 AM

The best thing we did was render the normalized filter contract in a dev-only panel.

Once you can *see* { q, tags, sort }, you stop arguing about whether the URL is lying.

Hana Saito
Sep 17, 2022 · 04:54 AM

Don’t keep filter state in local component state if back/forward needs to reproduce it.

Either make URL truth or make route doc truth, but don’t build a third truth in a hook.

Rowan Pierce
Sep 17, 2022 · 05:06 AM

We avoid URL-sync effects by letting navigation own the write, not an effect.

tsx
const hint = useRouteState({ q: '', sort: 'relevance' });
const doc = useRouteDoc('search');
doc.write('filters', { q: hint.read('q'), sort: hint.read('sort') });
return <div data-filters={JSON.stringify(doc.read('filters'))} />;

The key is there’s no “when doc changes, update URL” effect. The URL is only written on intent.

Grace McCoy
Sep 17, 2022 · 05:19 AM

Also: watch out for empty values. ?q= vs missing q can cause subtle diffs if you treat the raw params as meaning.

Normalize aggressively and render the normalized shape.

Inez Park
Sep 17, 2022 · 05:31 AM

If you have a “default sort” and you don’t render it, you’ll get “why did it change” tickets.

Even a tiny <div data-sort='relevance' /> is enough to make it debuggable.

Benji Rios
Sep 17, 2022 · 05:44 AM

One last tip: if product wants pretty URLs, keep pretty on the outside and boring on the inside.

Parse into a contract, then only render from the contract.