UI Patterns
Hooks

URL State

Patterns for persisting application state in the URL — query params, compression, and deep links.

Storing UI state in the URL makes it shareable, bookmarkable, and back-button-friendly. The URL Dialog page covers the simplest case — a single boolean toggle. This page covers the general problem: arbitrary state in query parameters, and what to do when it gets too large.

Query params with nuqs

nuqs is a type-safe search-params state manager for React (~6 kB gzipped). It gives you a useState-like API where the value lives in the URL instead of component state:

import { useQueryState, parseAsInteger } from "nuqs";

const [search, setSearch] = useQueryState("q", { defaultValue: "" });
const [page, setPage] = useQueryState("page", parseAsInteger.withDefault(1));

Key features:

  • Built-in parsers for strings, integers, floats, booleans, enums, dates, JSON, and more. Custom serializers are straightforward.
  • History control — choose push (adds a back-stack entry) or replace (overwrites the current one) per setter call.
  • Batched updates via useQueryStates — update multiple params in a single URL write, avoiding intermediate states.
  • Framework support — Next.js (App Router + Pages Router), Remix, React Router, TanStack Router, and plain React SPAs.
  • Server-component safe — shallow updates (the default) don't trigger a server round-trip; opt in to server re-rendering when needed.

When to reach for nuqs

ScenarioTool
One boolean (modal open/close)useUrlToggle — zero deps, minimal
A handful of typed params (search, filters, page)nuqs — built-in parsers, batching, framework integration
Complex nested object or very large stateCompression (see below) or server-side storage with a URL key

Compressing state into the URL

When the state outgrows a few simple params — think a full editor layout, a graph visualization, or a playground config — you can compress it into a single query parameter:

  1. JSON.stringify the state object.
  2. Compress with a deflate library (pako, the browser's built-in CompressionStream, or a text-specific compressor).
  3. Base64-encode the result for URL safety.
import pako from "pako";

function encode(state: unknown): string {
  const json = JSON.stringify(state);
  const compressed = pako.deflate(json);
  return btoa(String.fromCharCode(...compressed));
}

function decode<T>(encoded: string): T {
  const bytes = Uint8Array.from(atob(encoded), (c) => c.charCodeAt(0));
  const json = pako.inflate(bytes, { to: "string" });
  return JSON.parse(json);
}

Usage:

// Save
const url = new URL(window.location.href);
url.searchParams.set("s", encode(appState));
history.replaceState(null, "", url);

// Restore
const encoded = new URL(window.location.href).searchParams.get("s");
if (encoded) loadState(decode(encoded));

URL length limits

Compression helps, but URLs are not unlimited:

EnvironmentPractical limit
Older proxies / CDNs / email clients~2 000 chars
Internet Explorer (legacy)~2 083 chars
Modern Chrome~32 000 chars
Safari~64 000 chars
Firefox~300 000+ chars

The safe ceiling is ~2 000 characters if you need to support copy-paste through email, Slack previews, or QR codes. Deflate typically achieves 40–50% compression on JSON, so you can fit roughly 1.5 KB of raw state in a URL-safe payload.

Text-specific compression

General-purpose deflate compresses bytes. If your state is predominantly text (think: a code playground, a markdown editor, a DSL config), a compressor designed for short strings can do better:

  • Unishox2 — a hybrid entropy + dictionary compressor optimised for short Unicode strings. Achieves higher compression ratios than deflate on strings under ~1 KB, especially those with natural-language patterns or repeated Unicode code points.
  • lz-string — LZ-based compression with a built-in compressToEncodedURIComponent output mode that avoids the base64 round-trip entirely.
  • smol-string — Huffman + back-reference compressor for short strings, outputs URL-safe base64 directly.

Further reading

On this page