← 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.