DEV Community

Cover image for The URL Is Your Best State Manager
Parsa Jiravand
Parsa Jiravand

Posted on

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.

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:

  1. Refresh survival. The user filters a table, refreshes — the filter is still there.
  2. Shareable links. "Here's what I'm looking at" works via copy-paste.
  3. 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)}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode
// 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; })
      }
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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)