Use Cases (Diperbarui: 2/6/2026)

Membuat Pagination dengan Claude Code, React, dan Next.js

Panduan pagination dengan Claude Code: URL state, API metadata, aksesibilitas, dan edge case.

Membuat Pagination dengan Claude Code, React, dan Next.js

Pagination terlihat seperti pola UI kecil: previous, next, dan deretan nomor halaman. Di production, bagian yang sering rusak bukan bentuk tombolnya, tetapi keputusan bahwa URL adalah sumber kebenaran, filter pencarian tetap terbawa, nomor halaman invalid dikoreksi, halaman terakhir tetap aman saat data berubah, metadata API bisa dipercaya, dan screen reader memahami halaman mana yang sedang aktif.

Saat mencoba pola ini di arsip artikel dan tabel bergaya admin ClaudeCodeLab, masalah yang sama sering muncul dari hasil pertama: page=0 menampilkan daftar kosong, filter hilang saat pindah halaman, halaman terakhir salah setelah data dihapus, dan halaman aktif hanya ditandai warna. Claude Code bisa menghasilkan versi yang lebih kuat, tetapi prompt awal harus memuat aturan batas dan cara verifikasi.

Panduan ini menjelaskan cara meminta Claude Code membuat pagination yang layak dipakai dengan React dan Next.js App Router. Isinya mencakup prompt, desain URL, pemotongan data di server, JSON API, komponen yang aksesibel, lebih dari tiga use case, jebakan konkret, tautan resmi, tautan internal, CTA, dan catatan hasil uji. Jika daftar perlu memuat item terus menerus, bandingkan dengan implementasi infinite scroll. Untuk desain endpoint, baca panduan REST API. Untuk keyboard dan screen reader, lihat implementasi aksesibilitas.

Pilih model pagination

Ada dua model utama. Offset pagination meminta “halaman 3, 10 item per halaman”. Model ini cocok untuk arsip artikel, hasil pencarian, daftar produk, dan tabel admin karena setiap halaman dapat memiliki URL stabil. Cursor pagination meminta “10 item setelah ID ini”. Model ini lebih cocok untuk notifikasi, audit log, chat, dan timeline yang berubah saat pengguna membaca.

Artikel ini memakai offset pagination karena lebih ramah untuk SEO, riwayat browser, dan tautan yang dibagikan. URL seperti /articles?page=3&q=react bisa dibuka langsung, dikirim ke rekan kerja, dan dipulihkan setelah refresh. Untuk feed real-time, minta Claude Code memakai cursor secara eksplisit agar tidak muncul duplikasi atau item yang terlewat.

ModelCocok untukRisiko utama
OffsetArtikel, pencarian, produk, tabel adminHalaman terakhir bergeser saat total berubah
CursorNotifikasi, log, chat, timelineLompat ke halaman bebas lebih sulit
Infinite scrollFeed, galeri, konten terkaitTombol kembali, footer, dan SEO butuh perhatian ekstra

Dokumentasi resmi Claude Code Overview menjelaskan Claude Code sebagai agentic coding tool yang bisa membaca codebase, mengedit file, menjalankan command, dan terhubung dengan tool development. Karena itu, minta fitur lengkap: URL, API, aksesibilitas, dan verifikasi, bukan hanya komponen visual.

Prompt untuk Claude Code

Pagination menyentuh UI, routing, data, dan aksesibilitas. Prompt awal harus menjelaskan definisi selesai.

Implementasikan pagination daftar artikel dengan React dan Next.js App Router.
Syarat:
- Gunakan parameter URL page dan q sebagai sumber kebenaran
- Dukung searchParams sebagai Promise di page.tsx untuk Next.js modern
- Tampilkan 10 item per halaman; page=0 atau non-numeric kembali ke 1
- Jika halaman yang diminta melewati halaman terakhir, tampilkan halaman terakhir
- Tambahkan aria-current="page" pada link halaman aktif
- Render previous/next yang disabled sebagai span, bukan link yang bisa diklik
- Jangan merusak frontmatter, heroImage, internal link, atau localized route
- Setelah implementasi, tuliskan edge case yang perlu diuji

Pada App Router modern Next.js, searchParams di page.tsx diperlakukan sebagai Promise. Referensi resmi page.js menunjukkan pola membaca dengan await. Untuk Client Component, useSearchParams membaca query string, tetapi hasilnya adalah URLSearchParams read-only.

State di URL

Contoh berikut berjalan sebagai Server Component. Ia membaca q dan page, menormalkan nomor halaman, menjaga filter, lalu mengirim nilai aman ke komponen Pagination. Data memakai array agar mudah dicoba; di aplikasi nyata, ganti filter dan slice dengan query database yang memakai aturan sama.

import { Pagination } from "@/components/Pagination";

const PAGE_SIZE = 10;

const articles = Array.from({ length: 87 }, (_, index) => ({
  id: `article-${index + 1}`,
  title: `Claude Code pagination note ${index + 1}`,
  createdAt: new Date(Date.UTC(2026, 0, index + 1)).toISOString(),
}));

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

function readPage(value: string | undefined) {
  const page = Number(value ?? "1");
  return Number.isInteger(page) && page > 0 ? page : 1;
}

export default async function ArticlesPage({
  searchParams,
}: {
  searchParams: SearchParams;
}) {
  const params = await searchParams;
  const query = params.q?.trim() ?? "";
  const requestedPage = readPage(params.page);

  const filtered = query
    ? articles.filter((article) =>
        article.title.toLowerCase().includes(query.toLowerCase()),
      )
    : articles;

  const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
  const currentPage = Math.min(requestedPage, totalPages);
  const start = (currentPage - 1) * PAGE_SIZE;
  const visibleArticles = filtered.slice(start, start + PAGE_SIZE);

  return (
    <main className="mx-auto max-w-3xl px-4 py-10">
      <h1 className="text-3xl font-bold">Articles</h1>

      <form action="/articles" className="mt-6 flex gap-2">
        <input
          type="search"
          name="q"
          defaultValue={query}
          placeholder="Search articles"
          className="min-w-0 flex-1 rounded border px-3 py-2"
        />
        <button className="rounded bg-black px-4 py-2 text-white">Search</button>
      </form>

      <p className="mt-4 text-sm text-gray-600">
        {filtered.length} articles, page {currentPage} of {totalPages}
      </p>

      <ul className="mt-6 divide-y">
        {visibleArticles.map((article) => (
          <li key={article.id} className="py-4">
            <h2 className="font-semibold">{article.title}</h2>
            <time className="text-sm text-gray-500" dateTime={article.createdAt}>
              {new Intl.DateTimeFormat("en").format(new Date(article.createdAt))}
            </time>
          </li>
        ))}
      </ul>

      <Pagination
        currentPage={currentPage}
        totalPages={totalPages}
        basePath="/articles"
        query={{ q: query || undefined }}
      />
    </main>
  );
}

Keputusan pentingnya adalah tidak menyimpan halaman hanya di React state. Jika state tidak ada di URL, refresh, share link, indexing, dan tombol back menjadi rapuh. URLSearchParams adalah API standar untuk query string; referensi dasarnya ada di MDN URLSearchParams.

JSON API

Jika mobile app, widget dashboard, atau tabel client juga perlu data yang sama, sediakan API dengan metadata eksplisit. Jangan menerima pageSize apa pun dari pengguna.

import type { NextRequest } from "next/server";

const MAX_PAGE_SIZE = 50;

const articles = Array.from({ length: 87 }, (_, index) => ({
  id: `article-${index + 1}`,
  title: `Claude Code pagination note ${index + 1}`,
  createdAt: new Date(Date.UTC(2026, 0, index + 1)).toISOString(),
}));

function readPositiveInt(value: string | null, fallback: number) {
  const parsed = Number(value);
  return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}

export async function GET(request: NextRequest) {
  const page = readPositiveInt(request.nextUrl.searchParams.get("page"), 1);
  const requestedSize = readPositiveInt(
    request.nextUrl.searchParams.get("pageSize"),
    10,
  );
  const pageSize = Math.min(requestedSize, MAX_PAGE_SIZE);
  const totalItems = articles.length;
  const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));
  const safePage = Math.min(page, totalPages);
  const start = (safePage - 1) * pageSize;

  return Response.json({
    items: articles.slice(start, start + pageSize),
    meta: {
      page: safePage,
      pageSize,
      totalItems,
      totalPages,
      hasPreviousPage: safePage > 1,
      hasNextPage: safePage < totalPages,
    },
  });
}

Letakkan di app/api/articles/route.ts. Dokumentasi resmi Next.js route handlers menjelaskan pola route.ts di dalam direktori app. Di production, data layer harus mengembalikan baris yang terlihat dan total yang akurat atau perkiraan yang memang diterima produk.

Komponen aksesibel

Gaya visual boleh berubah, tetapi semantik harus stabil. Gunakan nav dengan label, tandai tepat satu halaman aktif dengan aria-current="page", dan jangan biarkan kontrol disabled tetap berupa link aktif. Referensi MDN aria-current menyebut pagination sebagai salah satu contoh penggunaan.

import Link from "next/link";

type QueryValue = string | number | undefined;

type PaginationProps = {
  currentPage: number;
  totalPages: number;
  basePath: string;
  query?: Record<string, QueryValue>;
  previousLabel?: string;
  nextLabel?: string;
};

function normalizePage(page: number, totalPages: number) {
  return Math.min(Math.max(1, page), Math.max(1, totalPages));
}

function visiblePages(currentPage: number, totalPages: number) {
  const pages = new Set([1, totalPages, currentPage - 1, currentPage, currentPage + 1]);
  return [...pages]
    .filter((page) => page >= 1 && page <= totalPages)
    .sort((a, b) => a - b);
}

function hrefForPage(
  basePath: string,
  query: Record<string, QueryValue>,
  page: number,
) {
  const params = new URLSearchParams();

  for (const [key, value] of Object.entries(query)) {
    if (value !== undefined && value !== "") params.set(key, String(value));
  }

  if (page === 1) {
    params.delete("page");
  } else {
    params.set("page", String(page));
  }

  const queryString = params.toString();
  return queryString ? `${basePath}?${queryString}` : basePath;
}

export function Pagination({
  currentPage,
  totalPages,
  basePath,
  query = {},
  previousLabel = "Previous",
  nextLabel = "Next",
}: PaginationProps) {
  if (totalPages <= 1) return null;

  const safePage = normalizePage(currentPage, totalPages);
  const pages = visiblePages(safePage, totalPages);

  return (
    <nav className="mt-8" aria-label="Pagination">
      <ol className="flex flex-wrap items-center gap-2">
        <li>
          {safePage === 1 ? (
            <span aria-disabled="true" className="rounded border px-3 py-2 opacity-50">
              {previousLabel}
            </span>
          ) : (
            <Link
              className="rounded border px-3 py-2 hover:bg-gray-50"
              href={hrefForPage(basePath, query, safePage - 1)}
            >
              {previousLabel}
            </Link>
          )}
        </li>

        {pages.map((page, index) => {
          const previous = pages[index - 1];
          const needsGap = previous !== undefined && page - previous > 1;

          return (
            <li key={page} className="flex items-center gap-2">
              {needsGap ? <span aria-hidden="true">...</span> : null}
              <Link
                aria-current={page === safePage ? "page" : undefined}
                className={
                  page === safePage
                    ? "rounded border bg-black px-3 py-2 text-white"
                    : "rounded border px-3 py-2 hover:bg-gray-50"
                }
                href={hrefForPage(basePath, query, page)}
              >
                {page}
              </Link>
            </li>
          );
        })}

        <li>
          {safePage === totalPages ? (
            <span aria-disabled="true" className="rounded border px-3 py-2 opacity-50">
              {nextLabel}
            </span>
          ) : (
            <Link
              className="rounded border px-3 py-2 hover:bg-gray-50"
              href={hrefForPage(basePath, query, safePage + 1)}
            >
              {nextLabel}
            </Link>
          )}
        </li>
      </ol>
    </nav>
  );
}

Diagram dan review

Setelah implementasi, minta Claude Code membuat diagram kecil. Diagram membuat tanggung jawab URL, server page, list, dan komponen lebih mudah diperiksa.

flowchart LR
  A["URL: /articles?page=3&q=react"] --> B["Page searchParams"]
  B --> C["readPage and filter"]
  C --> D["slice visible items"]
  D --> E["Article list"]
  C --> F["Pagination component"]
  F --> A
  C --> G["Optional JSON API meta"]

Pertanyaan review-nya sederhana: apakah semua state bisa dibangun kembali dari URL? Jika ya, refresh, tombol back, berbagi link, dan SEO menjadi lebih aman.

Use case nyata

Use case pertama adalah blog atau arsip dokumentasi. Ketika jumlah tutorial Claude Code bertambah, halaman awal tetap ringan dan artikel lama tetap bisa dibuka lewat link langsung.

Use case kedua adalah pencarian ecommerce atau SaaS. Teks pencarian, kategori, harga, urutan, dan halaman harus tetap ada di URL. Minta Claude Code mengembalikan page ke 1 saat filter berubah.

Use case ketiga adalah tabel admin: invoice, user, form submission, atau audit record. Di sini penting untuk membatasi page size, menerapkan filter permission, dan menjaga konsistensi dengan export CSV.

Use case keempat adalah dashboard pembelajaran. Pembaca bisa kembali beberapa hari kemudian. Pagination yang stabil menjaga posisi dan memudahkan kembali dari CTA seperti cheat sheet gratis atau konsultasi Claude Code.

Jebakan umum

Jangan percaya nomor halaman dari request. page=-1, page=abc, dan page=9999 harus ditangani di server. Jangan membuang filter saat membuat link; ganti hanya parameter page. Jangan menandai halaman aktif hanya dengan warna; gunakan tepat satu aria-current="page". Jangan abaikan biaya count di tabel besar; COUNT(*) bisa membutuhkan index, cache, atau approximate count.

Tentukan juga perilaku history. API tingkat rendah pushState() menambahkan entri ke session history browser, seperti dijelaskan di MDN History pushState. Di Next.js, biasanya gunakan Link atau router.push, tetapi pilih dengan sadar apakah perubahan halaman harus menambah entry atau mengganti entry saat ini.

Hasil verifikasi

Saya memeriksa contoh ini dengan kasus: tanpa page, page=1, page=0, page=abc, page=9999, pencarian dengan hasil, pencarian tanpa hasil, halaman terakhir, dan hasil yang hanya satu halaman. Detail yang paling berguna adalah menghapus page=1 dari URL dan menjepit halaman yang terlalu besar setelah filter. Link yang dibagikan jadi lebih bersih dan layar kosong lebih jarang muncul saat data berubah.

Sebelum publish, minta Claude Code melakukan review akhir: hanya satu aria-current, previous/next disabled di batas, pageSize dibatasi, filter tetap terbawa, searchParams dibaca dengan await, dan contoh TypeScript valid. Pagination memang kecil, tetapi berdampak pada arsip, pencarian, admin tool, dan jalur konversi.

#Claude Code #pagination #React #Next.js #UX
Gratis

PDF gratis: cheatsheet Claude Code

Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.

Kami menjaga datamu dan tidak mengirim spam.

Masa

Tentang penulis

Masa

Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.