Keep UI state in the URL

Put filters, tabs, pages and open panels in query params.

From web-design-guidelines (Web Interface Guidelines). Library: nuqs.

Reload the page

Filter, page and search, then press Reload.

State in memoryBad
useState("all")
State in the URLGood
useQueryState("status")
const [status, setStatus] = useQueryState("status",
  parseAsStringLiteral(STATUSES).withDefault("all").withOptions({ history: "push" }));
const [page, setPage] = useQueryState("page", parseAsInteger.withDefault(1));
const [q, setQ] = useQueryState("q", { defaultValue: "" });

Share a link

Open Payments, expand a row, then press Share both.

State in memoryBad
useState("overview")
State in the URLGood
?tab=payments&open=42
function setView(patch) {
  const url = new URL(location.href);
  for (const [k, v] of Object.entries(patch)) v == null ? url.searchParams.delete(k) : url.searchParams.set(k, v);
  history.pushState(null, "", url);
  render(readView());
}
addEventListener("popstate", () => render(readView()));

Press Back

Switch tabs a few times, then press Back.

State in memoryBad
useState("general")
State in the URLGood
history: "push" | "replace"
const [tab, setTab] = useQueryState("tab",
  parseAsStringLiteral(TABS).withDefault("general").withOptions({ history: "push" }));
const [q, setQ] = useQueryState("q",
  parseAsString.withDefault("").withOptions({ history: "replace" }));

Why it matters, pros and cons

Why it matters

The URL is the only state the browser saves and shares; memory dies on reload.

Pros

  • Reload restores the exact view.
  • A link shows exactly what you saw.
  • Back and Forward step through views.

Cons

  • Skip transient UI: hover, open menus, drafts.
  • Never put secrets in URLs; they get logged.
  • Params are user input; validate them.