Next.js cookbook · Beginner

Paginate and filter with searchParams in Server Components

Build a searchable, paginated product list rendered on the server and shareable via URL.

Approach

Read searchParams (a Promise in Next.js 15+) in the page, validate it, and fetch on the server. A small client component updates the URL with useRouter().replace() (debounced for search), which re-renders the server page. Wrap results in Suspense keyed by the params for loading states.

Solution

// app/products/page.tsx
import { Suspense } from "react";

export default async function ProductsPage({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) {
  const sp = await searchParams;
  const q = (sp.q ?? "").slice(0, 100);
  const page = Math.max(1, Number(sp.page) || 1);
  return (
    <main>
      <SearchBox defaultValue={q} />
      <Suspense key={`${q}-${page}`} fallback={<ResultsSkeleton />}>
        <Results q={q} page={page} />
      </Suspense>
    </main>
  );
}

async function Results({ q, page }: { q: string; page: number }) {
  const { items, totalPages } = await searchProducts({ q, page, pageSize: 24 });
  return (<><ProductGrid items={items} /><Pagination page={page} totalPages={totalPages} q={q} /></>);
}

// app/products/SearchBox.tsx
"use client";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useDebouncedCallback } from "use-debounce";

export function SearchBox({ defaultValue }: { defaultValue: string }) {
  const router = useRouter();
  const pathname = usePathname();
  const params = useSearchParams();
  const onChange = useDebouncedCallback((value: string) => {
    const next = new URLSearchParams(params);
    value ? next.set("q", value) : next.delete("q");
    next.delete("page");
    router.replace(`${pathname}?${next}`);
  }, 300);
  return <input type="search" aria-label="Search products" defaultValue={defaultValue} onChange={(e) => onChange(e.target.value)} />;
}

Pitfalls

  • Forgetting await searchParams in Next.js 15+.
  • Pushing history entries on every keystroke.
  • Using searchParams makes the page dynamic — cache the data layer instead.

Variations

  • nuqs for type-safe URL state in Next.js.

Further reading: Next.js: page searchParams

Reference: https://nextjs.org/docs/app/api-reference/file-conventions/page#searchparams-optional

More Next.js recipes