← Back to docs
Recipe: URL query-param state sync
Keep UI state in the URL so deep links, back/forward navigation, and shareable bookmarks all work without a database.
The pattern
Read search params on the server with searchParams (page) or useSearchParams (client). Push new params with router.replace so the URL always reflects the current view.
Server-side read
export default async function Page({
searchParams,
}: {
searchParams: { tab?: string; page?: string };
}) {
const tab = searchParams.tab ?? "overview";
const page = parseInt(searchParams.page ?? "1", 10);
// fetch data, render
}Client-side update
"use client";
import { useRouter, useSearchParams } from "next/navigation";
export function TabBar() {
const router = useRouter();
const params = useSearchParams();
const current = params.get("tab") ?? "overview";
function switchTab(tab: string) {
const next = new URLSearchParams(params);
next.set("tab", tab);
router.replace(`?${next.toString()}`, { scroll: false });
}
return (
<div className="flex gap-2">
{["overview", "billing", "team"].map((t) => (
<button
key={t}
onClick={() => switchTab(t)}
className={`rounded px-4 py-2 text-sm ${
current === t
? "bg-[#8B5CF6] text-white"
: "bg-[#1a1228] text-gray-400"
}`}
>
{t}
</button>
))}
</div>
);
}Why it matters
- Browser back/forward works without state machines.
- Users can bookmark a filtered view and return to it.
- Zero server storage — the URL is the source of truth.