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.
What belongs in the URL
Not all state belongs there. The key question is: would a user want to share or bookmark this?
| State | URL? | Why |
|---|---|---|
| Active tab / current view | Yes | Direct-linkable, shareable |
| Search query | Yes | Back button works, shareable |
| Filters, sort order | Yes | Reproducible results |
| Pagination cursor | Yes | "Send me the page you're on" |
| Form draft in progress | No | Ephemeral, user-session only |
| Hover state, tooltip open | No | Ephemeral UI state |
| Auth tokens | Never | Security 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.
What you gain for free
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.
The pattern in React
Here's the before/after for a search input:
// Before: state dies on refresh
function SearchPage() {
const [query, setQuery] = useState("");
return (
<input
value={query}
onChange={e => setQuery(e.target.value)}
/>
);
}
// 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:
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.
The failure mode to watch for
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 takeaway
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.
Top comments (0)