← blog
JavaScriptJune 29, 2026 · 4 min read

The URL Is Your Best State Manager

You're storing filter state, search queries, and pagination in useState — and losing it every time someone refreshes. The URL already does this better.

Parsa Jiravand · Frontend engineer · building bestpractic
The URL Is Your Best State Manager

There's a category of bug I see in almost every SPA I touch: the user applies a filter, sorts a table, pages through results — and then refreshes, or pastes the link to a colleague, and all of it is gone. The bug report says "it doesn't remember my state." The root cause is almost always the same: that state lived in useState, when it should have lived in the URL.

This is a mental model, not a framework feature. It applies equally in React, Vue, Svelte, or plain JS. The URL is a state management primitive you already have — built into every browser, shared for free — and most frontend teams underuse it.

Not all state belongs there. The key question is: would a user want to share or bookmark this?

StateURL?Why
Active tab / current viewYesDirect-linkable, shareable
Search queryYesBack button works, shareable
Filters, sort orderYesReproducible results
Pagination cursorYes"Send me the page you're on"
Form draft in progressNoEphemeral, user-session only
Hover state, tooltip openNoEphemeral UI state
Auth tokensNeverSecurity hazard

The filter and the modal-open state are both "UI state," but they're different in kind. One the user would share; one they wouldn't. Put the first in the URL and leave the second in component state.

When search/filter/sort state lives in the URL, you get three things without writing a single extra line of code:

  • Refresh survival. The user filters a table, refreshes — the filter is still there.
  • Shareable links. "Here's what I'm looking at" works via copy-paste.
  • Browser history. Back and forward navigate state transitions, which is what users expect.

These are not small wins. "Back button broke my filters" is a real UX bug, and moving to URL state fixes it completely and permanently.

Here's the before/after for a search input:

TSX
1
2
3
4
5
6
7
8
9
10
// Before: state dies on refresh function SearchPage() { const [query, setQuery] = useState(""); return ( <input value={query} onChange={e => setQuery(e.target.value)} /> ); }
TSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// After: state lives in the URL import { useSearchParams } from "react-router-dom"; // or Next.js's useSearchParams function SearchPage() { const [params, setParams] = useSearchParams(); const query = params.get("q") ?? ""; return ( <input value={query} onChange={e => setParams(p => { p.set("q", e.target.value); return p; }) } /> ); }

The component is roughly the same size. The URL is now ?q=typescript. Refresh — still there. Copy the URL — the recipient sees the same results. The back button undoes the last keystroke (or use replace instead of push to keep history clean for every keystroke).

For more complex state — multiple filters, sort columns, numeric values — a library like nuqs handles serialization, type coercion, and history mode in a useState-compatible API:

TSX
1
2
3
4
import { useQueryState, parseAsInteger } from "nuqs"; const [sort, setSort] = useQueryState("sort", { defaultValue: "date" }); const [page, setPage] = useQueryState("page", parseAsInteger.withDefault(1));

Typed URL params, familiar API, zero custom serialization logic.

Don't put too much in the URL. It's for state the user would meaningfully share — not for every piece of transient UI. A deeply nested JSON blob in a query string is the wrong answer. Keep it flat, keep it readable.

?status=active&sort=name&page=2 is good. ?state=eyJzdGF0dXMiOiJhY3RpdmUi... is a red flag.

If your state is too complex to flatten into a few readable params, it may genuinely belong in a database or session store, not the URL. The URL should be a narrow representation of "what the user is looking at" — not a full snapshot of the app.

The URL is a first-class state management layer that every browser already implements, every user already understands, and most SPAs leave mostly unused. Before you reach for useState, ask: would the user want to share or bookmark this? If yes, put it in the URL — you get refresh survival, deep links, and a working back button for free.

That's not a third-party library or a framework feature. It's how the web was designed. Use it.


Thanks for reading! Let's stay connected:

Originally published on dev.to