Use Cases (Mis à jour: 02/06/2026)

Implémenter une pagination avec Claude Code, React et Next.js

Guide pratique pour créer une pagination avec Claude Code: URL, API, accessibilité et tests de bord.

Implémenter une pagination avec Claude Code, React et Next.js

La pagination semble être un petit composant: précédent, suivant, puis quelques numéros. En production, le vrai sujet n’est pas le style des boutons. Le vrai sujet est de choisir l’URL comme source de vérité, de conserver les filtres, de corriger les pages invalides, de gérer une dernière page qui change, de fournir des métadonnées API fiables et d’indiquer la page courante aux technologies d’assistance.

Sur les archives d’articles et les écrans de type administration de ClaudeCodeLab, les mêmes erreurs reviennent souvent après une première génération: page=0 affiche une liste vide, le filtre disparaît dans les liens, la dernière page casse après une suppression, ou la page courante n’est signalée que par la couleur. Claude Code peut éviter ces défauts, à condition de recevoir un contrat clair dès le départ.

Ce guide montre comment demander à Claude Code une pagination solide avec React et Next.js App Router. Il couvre le prompt, le design d’URL, le découpage côté serveur, une route JSON, un composant accessible, plus de trois cas d’usage, les pièges concrets, les références officielles, les liens internes, un CTA et une note de vérification. Pour un chargement continu, comparez avec l’implémentation du scroll infini. Pour le contrat d’API, consultez le guide de conception REST. Pour le clavier et les lecteurs d’écran, relisez aussi l’accessibilité avec Claude Code.

Choisir le modèle

Il existe deux modèles principaux. La pagination par offset demande “page 3, 10 éléments par page”. Elle convient aux archives, résultats de recherche, catalogues et tableaux d’administration, car chaque page peut avoir une URL stable. La pagination par curseur demande “les 10 éléments après cet ID”. Elle est plus adaptée aux notifications, journaux d’audit, discussions et timelines qui changent pendant la lecture.

Ici, nous utilisons l’offset, car il fonctionne bien pour le SEO, l’historique du navigateur et le partage. Une URL comme /articles?page=3&q=react peut être ouverte directement, envoyée à un collègue et restaurée après rechargement. Pour un flux en temps réel, dites explicitement à Claude Code d’utiliser un curseur, sinon les insertions récentes peuvent créer des doublons ou des trous.

ModèleBon usageRisque principal
OffsetArticles, recherche, produits, tableaux adminLa dernière page bouge quand le total change
CurseurNotifications, audits, chat, timelineLe saut direct vers une page arbitraire est difficile
Scroll infiniFeeds, galeries, contenus liésRetour navigateur, pied de page et SEO demandent plus de travail

La page officielle Claude Code Overview décrit Claude Code comme un outil de développement agentique capable de lire le code, modifier des fichiers, exécuter des commandes et s’intégrer aux outils de développement. Il faut donc lui confier le contrat complet: URL, API, accessibilité et vérification.

Prompt pour Claude Code

La pagination traverse l’UI, le routage, la donnée et l’accessibilité. Le prompt initial doit définir ce qui est terminé.

Implémente une pagination de liste d'articles avec React et Next.js App Router.
Contraintes:
- Utiliser les paramètres URL page et q comme source de vérité
- Supporter searchParams comme Promise dans page.tsx pour Next.js moderne
- Afficher 10 éléments par page; page=0 ou non numérique revient à 1
- Si la page demandée dépasse la dernière page, afficher la dernière
- Ajouter aria-current="page" au lien de la page courante
- Rendre précédent/suivant désactivés comme span, pas comme liens cliquables
- Ne pas casser frontmatter, heroImage, liens internes ni routes localisées
- Lister les tests de bord à exécuter après l'implémentation

Dans l’App Router moderne de Next.js, searchParams dans page.tsx est traité comme une Promise. La référence officielle page.js montre l’accès avec await. En composant client, useSearchParams lit la query string, mais renvoie une vue en lecture seule de URLSearchParams.

État dans l’URL

L’exemple suivant fonctionne côté serveur. Il lit q et page, normalise le numéro de page, conserve le filtre et transmet des valeurs sûres au composant Pagination. Les données sont un tableau pour rester copiables; en production, remplacez le filtrage et le slice par une requête de base de données.

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

Le point important est d’éviter un état caché uniquement dans React. Si la page n’existe pas dans l’URL, le rechargement, le partage, l’indexation et le bouton retour deviennent fragiles. URLSearchParams est l’API standard pour manipuler les query strings; la référence de base est MDN URLSearchParams.

Route JSON

Si une application mobile, un widget ou un tableau client consomme les mêmes données, exposez une API avec des métadonnées explicites. Ne laissez pas l’utilisateur choisir librement pageSize.

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

Placez ce fichier dans app/api/articles/route.ts. La documentation officielle des Next.js route handlers explique comment définir route.ts dans app. En production, la couche de données doit fournir les lignes visibles et un total exact ou approximatif assumé.

Composant accessible

Le style peut changer, mais la sémantique doit rester stable. Utilisez un nav nommé, marquez une seule page courante avec aria-current="page" et ne laissez pas les contrôles désactivés comme liens actifs. La référence MDN aria-current cite la pagination comme usage de aria-current="page".

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

Schéma de flux

Demandez à Claude Code un petit schéma après l’implémentation. Il rend visibles les responsabilités entre URL, page serveur, liste et composant.

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 question de revue est directe: tout l’état peut-il être reconstruit depuis l’URL? Si oui, rechargement, retour, partage et SEO deviennent plus fiables.

Cas d’usage

Premier cas: un blog ou une documentation. Quand le nombre de tutoriels augmente, la première page reste rapide et les anciens contenus restent accessibles par lien direct.

Deuxième cas: la recherche ecommerce ou SaaS. Texte, catégorie, prix, tri et page doivent rester dans l’URL. Demandez à Claude Code de remettre page à 1 quand un filtre change.

Troisième cas: les tableaux d’administration, comme factures, utilisateurs, formulaires et journaux d’audit. Il faut un plafond de page, des filtres de permissions et une cohérence avec l’export CSV.

Quatrième cas: un parcours d’apprentissage. Un lecteur revient parfois plusieurs jours plus tard. Une pagination stable lui permet de retrouver sa position et de revenir depuis une CTA comme la fiche gratuite ou la consultation Claude Code.

Pièges fréquents

Ne faites pas confiance au numéro entrant: page=-1, page=abc et page=9999 doivent être traités côté serveur. Ne perdez pas les filtres quand vous construisez les liens; remplacez seulement page. Ne signalez pas la page courante uniquement par la couleur; ajoutez aria-current="page" à un seul lien. Ne sous-estimez pas le coût du comptage sur une grande table; un COUNT(*) exact peut nécessiter index, cache ou estimation.

Décidez aussi du comportement de l’historique. L’API bas niveau pushState() ajoute une entrée à l’historique de session du navigateur, comme l’explique MDN History pushState. Dans Next.js, utilisez en général Link ou router.push, mais choisissez explicitement entre pousser une entrée ou remplacer l’entrée actuelle.

Résultat vérifié

J’ai vérifié l’exemple avec: absence de page, page=1, page=0, page=abc, page=9999, recherche avec résultats, recherche sans résultat, dernière page et liste d’une seule page. Les deux détails les plus utiles sont la suppression de page=1 dans l’URL et le recalage d’une page trop haute après filtrage. Les liens partagés sont plus propres et les écrans vides diminuent quand les données changent.

Avant publication, demandez à Claude Code de vérifier qu’un seul lien possède aria-current, que précédent/suivant sont désactivés aux limites, que pageSize est plafonné, que les filtres sont conservés, que searchParams est attendu avec await et que les exemples TypeScript restent valides. La pagination est petite, mais elle touche les archives, la recherche, l’administration et les chemins de conversion.

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

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.