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.

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?
| 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.
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:
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:
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:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 💬 Discord — join the frontend best-practices community: discord.gg/d9KRhuAwQ
- 📸 Instagram — frontend best practices, daily: @bestpractice___
- 💼 LinkedIn — linkedin.com/in/parsa-jiravand
- ✉️ Email (work & contract inquiries): bestpractice2026@gmail.com
Originally published on dev.to