R
Rishtaara
Next.js: Zero to Full-Stack
Lesson 23 of 42Article16 min

Adding Search and Pagination

Search text and page numbers belong in the URL (?q=react&page=2), not only in useState. Users can bookmark, share, and use the back button. Server Components can read searchParams and filter on the server.

URL as state — searchable, shareable, refresh-safe

Search text and page numbers belong in the URL (?q=react&page=2), not only in useState. Users can bookmark, share, and use the back button. Server Components can read searchParams and filter on the server.

Real-life example: URL state is like writing the aisle and shelf number on a shopping list — anyone with the note finds the same spot.

searchParams in page.tsx

In the App Router, the page receives searchParams. Treat them as the source of truth for filters.

A plain <form method="GET"> without action submits to the same URL and updates query params — no client JS required for basic search.
app/invoices/page.tsx
import Link from "next/link";
import { sql } from "@vercel/postgres";

type Props = {
  searchParams: Promise<{ q?: string; page?: string }>;
};

const PAGE_SIZE = 10;

export default async function InvoicesPage({ searchParams }: Props) {
  const { q = "", page = "1" } = await searchParams;
  const current = Math.max(1, Number(page) || 1);
  const offset = (current - 1) * PAGE_SIZE;

  const { rows } = await sql`
    SELECT id, customer_name, amount, status
    FROM invoices
    WHERE customer_name ILIKE ${"%" + q + "%"}
    ORDER BY id DESC
    LIMIT ${PAGE_SIZE} OFFSET ${offset}
  `;

  return (
    <main>
      <form>
        <label htmlFor="q">Search customers</label>
        <input id="q" name="q" defaultValue={q} placeholder="Asha..." />
        <button type="submit">Search</button>
      </form>

      <ul>
        {rows.map((row) => (
          <li key={row.id}>
            {row.customer_name} — ₹{row.amount} ({row.status})
          </li>
        ))}
      </ul>

      <nav aria-label="Pagination">
        {current > 1 && (
          <Link href={`/invoices?q=${encodeURIComponent(q)}&page=${current - 1}`}>
            Previous
          </Link>
        )}
        <span>Page {current}</span>
        <Link href={`/invoices?q=${encodeURIComponent(q)}&page=${current + 1}`}>
          Next
        </Link>
      </nav>
    </main>
  );
}

Pagination with Link

Prefer <Link> for page changes so Next.js can prefetch. Keep q in every page link so filters survive when you flip pages.

Real-life example: Pagination links are page numbers in a book index — each link must remember which chapter (filter) you were reading.

  • Encode query strings with encodeURIComponent
  • Clamp page to >= 1
  • Disable or hide Next when the last page has fewer than PAGE_SIZE rows