Use Cases (Actualizado: 2/6/2026)

Implementar paginación con Claude Code, React y Next.js

Guía práctica para crear paginación con Claude Code: URL, API, accesibilidad y pruebas de borde.

Implementar paginación con Claude Code, React y Next.js

La paginación parece un patrón pequeño: anterior, siguiente y una fila de números. En producción, el problema no suele estar en pintar los botones, sino en decidir que la URL es la fuente de verdad, mantener los filtros de búsqueda, corregir páginas inválidas, comunicar la página actual a tecnologías de asistencia y devolver metadatos de API que la interfaz pueda confiar.

Cuando probé este patrón en archivos de artículos y pantallas tipo administración de ClaudeCodeLab, los fallos se repetían: page=0 producía una lista vacía, la última página se rompía después de borrar registros, el filtro de búsqueda desaparecía al avanzar y la página actual solo se distinguía por color. Claude Code puede evitarlo, pero hay que darle el contrato desde el primer prompt.

Esta guía muestra cómo pedir a Claude Code una paginación sólida con React y Next.js App Router. Incluye prompt, diseño de URL, corte de datos en servidor, ruta JSON, componente accesible, más de tres casos de uso, errores concretos, enlaces oficiales, enlaces internos, CTA y una nota de verificación real. Si tu lista debe cargar elementos de forma continua, compárala con scroll infinito. Para revisar el contrato de endpoints, lee diseño de REST API. Para teclado y lectores de pantalla, revisa accesibilidad con Claude Code.

Decidir el modelo

Hay dos modelos principales. La paginación por offset pide “página 3, 10 elementos por página”. Es adecuada para archivos de artículos, resultados de búsqueda, catálogos y tablas de administración, porque cada página puede tener una URL estable. La paginación por cursor pide “los 10 elementos después de este ID”. Funciona mejor para notificaciones, logs, chats y líneas de tiempo que cambian mientras el usuario lee.

Aquí usamos offset porque favorece SEO, historial del navegador y enlaces compartibles. Una URL como /articles?page=3&q=react puede abrirse directamente, enviarse a otra persona y restaurarse después de recargar. Para feeds en tiempo real, especifica a Claude Code que use cursor; si no, puedes ver duplicados o saltos cuando entran nuevos registros.

ModeloDónde encajaRiesgo principal
OffsetArtículos, búsqueda, productos, tablas adminLa última página cambia si cambia el total
CursorNotificaciones, logs, chat, timelineSaltar a una página arbitraria es difícil
Scroll infinitoFeeds, galerías, relacionadosHistorial, pie de página y SEO requieren más cuidado

La documentación oficial de Claude Code Overview lo presenta como una herramienta agentic que lee el código, edita archivos, ejecuta comandos e integra herramientas de desarrollo. Por eso conviene pedirle la función completa: URL, API, accesibilidad y verificación, no solo un componente visual.

Prompt para Claude Code

La paginación cruza UI, rutas, datos y accesibilidad. El prompt inicial debe fijar la definición de terminado.

Implementa paginación para una lista de artículos con React y Next.js App Router.
Requisitos:
- Usar los parámetros page y q de la URL como fuente de verdad
- Soportar searchParams como Promise en page.tsx para Next.js moderno
- Mostrar 10 elementos por página; page=0 o valores no numéricos vuelven a 1
- Si la página solicitada supera la última, mostrar la última página
- Añadir aria-current="page" al enlace de la página actual
- Renderizar anterior/siguiente deshabilitados como span, no como enlaces clicables
- No romper frontmatter, heroImage, enlaces internos ni rutas localizadas
- Al final, listar pruebas de borde que debo ejecutar

En el App Router moderno de Next.js, searchParams en page.tsx se trata como Promise. La referencia oficial de page.js muestra el acceso con await. En componentes de cliente puedes leer la query con useSearchParams, pero devuelve una vista de solo lectura de URLSearchParams.

Estado en la URL

El siguiente ejemplo funciona como componente de servidor. Lee q y page, normaliza la página, mantiene el filtro y pasa valores seguros al componente Pagination. El origen de datos es un array para que el ejemplo sea copiable; en un proyecto real se sustituye por una consulta a base de datos.

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>
  );
}

La decisión importante es no esconder la página en estado local de React. Si el número vive solo en memoria, recargar, compartir, indexar o volver atrás se vuelve frágil. URLSearchParams es la API estándar para trabajar con query strings; la referencia base es MDN URLSearchParams.

Ruta JSON

Si una app móvil, un widget o una tabla de cliente necesitan los mismos datos, expón una API con metadatos claros. No aceptes cualquier pageSize enviado por el usuario.

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,
    },
  });
}

Colócalo en app/api/articles/route.ts. La documentación oficial de Next.js route handlers explica cómo definir route.ts dentro del directorio app. En producción, el repositorio de datos debe devolver los registros visibles y un total confiable o una aproximación aceptada por el producto.

Componente accesible

El estilo puede cambiar, pero la semántica no. Usa nav con etiqueta, marca una sola página actual con aria-current="page" y no dejes los controles deshabilitados como enlaces activos. La referencia de MDN aria-current menciona explícitamente la paginación como caso de uso.

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>
  );
}

Diagrama y revisión

Pídele a Claude Code un diagrama pequeño después de implementar. Ayuda a revisar si la URL, el servidor, la API y el componente tienen responsabilidades separadas.

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"]

La pregunta de revisión es simple: ¿se puede reconstruir todo el estado desde la URL? Si la respuesta es sí, recarga, historial, compartir y SEO son más fiables.

Casos de uso

El primer caso es un blog o archivo de documentación. A medida que crecen los tutoriales, la primera página sigue siendo ligera y las páginas antiguas son accesibles por enlace directo.

El segundo caso es búsqueda en ecommerce o SaaS. Texto, categoría, precio, orden y página deben permanecer en la URL. Indica a Claude Code que al cambiar filtros debe volver a page=1; si no, el usuario puede quedarse en la página 5 y ver una lista vacía.

El tercer caso es una tabla de administración: facturas, usuarios, formularios o registros de auditoría. Aquí importan el límite de página, filtros de permisos y coherencia con exportaciones CSV.

El cuarto caso es un panel de aprendizaje. Un lector puede volver varios días después a una lista de lecciones. La paginación estable evita perder posición y permite volver desde una CTA como la hoja gratuita o consultoría de Claude Code.

Errores habituales

No confíes en el número recibido. page=-1, page=abc y page=9999 deben corregirse en servidor. No pierdas filtros al generar enlaces; cambia solo el parámetro page. No uses solo color para marcar la página actual; añade aria-current="page" a un único enlace. No ignores el coste del conteo en tablas grandes; un COUNT(*) exacto puede ser caro y quizá necesites índices, caché o conteos aproximados.

También decide el comportamiento del historial. La API de bajo nivel pushState() añade una entrada al historial de sesión del navegador, como explica MDN History pushState. En Next.js normalmente usarás Link o router.push, pero debes decidir si cada cambio de página crea una nueva entrada o reemplaza la actual.

Resultado verificado

Probé el ejemplo con estos casos: sin page, page=1, page=0, page=abc, page=9999, búsqueda con resultados, búsqueda sin resultados, última página y resultado de una sola página. Lo más útil fue eliminar page=1 de la URL y ajustar una página demasiado alta después de filtrar. Los enlaces compartidos quedan más limpios y se evitan pantallas vacías cuando cambian los datos.

Antes de publicar, pide a Claude Code una revisión final: un solo aria-current, controles anterior/siguiente deshabilitados en los bordes, límite de pageSize, filtros preservados, searchParams leído con await y ejemplos TypeScript válidos. La paginación es pequeña, pero afecta archivos, búsqueda, administración y rutas de conversión.

#Claude Code #paginación #React #Next.js #UX
Gratis

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.