Tips & Tricks (Mis à jour: 02/06/2026)

Scroll infini en production avec Claude Code et React

Implémentez le scroll infini avec Claude Code, React, API cursor, accessibilité, SEO et gestion d'échecs.

Scroll infini en production avec Claude Code et React

Le scroll infini charge de nouveaux éléments quand l’utilisateur approche de la fin d’une liste. Dans un flux social, une archive d’articles, un catalogue ou un journal d’activité, il semble évident. En production, pourtant, ce n’est pas seulement “déclencher un fetch quand la dernière carte apparaît”.

Les vrais problèmes arrivent avec les requêtes en double, les anciennes réponses qui écrasent l’état récent, la perte de position après un retour navigateur, les lecteurs d’écran sans indication, le SEO affaibli et les API qui répètent ou sautent des éléments quand de nouvelles données sont insérées. Si vous demandez simplement à Claude Code “ajoute un scroll infini”, vous obtiendrez souvent une démo correcte mais pas un composant prêt à publier.

Ce guide transforme la fonctionnalité en workflow clair pour Claude Code : prompt initial, hook React, composant de liste, route Next.js avec cursor, cas d’usage réels, pièges concrets, liens officiels et note de vérification. Pour les listes où le problème principal est le nombre de nœuds DOM, consultez aussi le scroll virtuel. Quand l’utilisateur doit choisir une page précise, comparez avec la pagination.

Décider Avant De Coder

Intersection Observer est une API du navigateur qui indique quand un élément cible croise le viewport ou un conteneur racine. Autrement dit, le navigateur vous dit quand le marqueur de fin de liste s’approche de l’écran. La référence à garder sous la main est MDN Intersection Observer API.

Le petit élément placé à la fin de la liste est souvent appelé sentinel. Lorsqu’il devient visible, l’application charge la page suivante. Cette approche est plus propre qu’un gestionnaire de scroll exécuté à chaque mouvement. Avec rootMargin, on peut lancer le chargement avant que l’utilisateur ne voie réellement la fin.

La deuxième décision est le type de pagination. L’offset pagination demande “saute 40 éléments puis prends les 20 suivants”. Elle fonctionne sur une liste figée, mais un flux vivant peut bouger entre deux appels. La cursor pagination demande “continue après cet id”, ce qui est généralement plus stable pour les articles, notifications, logs et résultats de recherche.

Un prompt utile pour Claude Code ressemble à ceci :

Implémente une liste d'articles avec scroll infini en React et Next.js.
Utilise Intersection Observer et une API basée sur cursor.
Inclus la prévention des requêtes en double, le cleanup AbortController,
un état d'erreur visible, un bouton manuel "Charger plus", aria-live,
role="feed" et des liens HTML normaux pour le SEO.
Ne supprime pas le frontmatter, heroImage, les liens internes ni les routes localisées.

Les workflows courants de Claude Code insistent sur des tâches claires, des exemples et des contraintes. Le scroll infini en a besoin, car il touche l’interface, l’API, l’accessibilité et le comportement produit.

Cas D’usage

Premier cas : l’archive d’articles. Un site de tutoriels peut garder une première page rapide tout en proposant davantage de contenu aux lecteurs engagés. Le point fragile est le retour depuis un article : si la position disparaît, l’expérience devient pénible.

Deuxième cas : la recherche ecommerce ou SaaS. Parcourir des produits, modèles ou intégrations se fait naturellement en continu. Mais les filtres, le tri et le texte de recherche doivent rester dans l’URL pour permettre le partage et la reprise.

Troisième cas : les notifications et journaux d’audit. Les opérateurs lisent souvent du plus récent au plus ancien. Ici, le cursor technique, l’horodatage et l’état “lu” doivent rester séparés. Ne mélangez pas le dernier élément vu avec un statut métier.

Quatrième cas : chat, commentaires et flux d’activité. Ces écrans ont parfois besoin d’un scroll infini inversé pour charger l’historique au-dessus. Précisez la direction à Claude Code, car la conservation de la position n’est pas la même.

Cinquième cas : un tableau de bord d’apprentissage. Les leçons, exemples et checklists peuvent s’enchaîner, mais chaque section doit garder une URL stable, un suivi de progression et une CTA comme la formation Claude Code.

Hook React

Le hook suivant suppose une API cursor. Il annule les requêtes anciennes avec AbortController, bloque les fetchs en double avec loadingRef et précharge grâce à rootMargin.

import { useCallback, useEffect, useRef, useState } from "react";

export type CursorPage<T> = {
  items: T[];
  nextCursor: string | null;
};

type FetchPage<T> = (args: {
  cursor: string | null;
  signal: AbortSignal;
}) => Promise<CursorPage<T>>;

type InfiniteStatus = "idle" | "loading" | "error" | "done";

type UseInfiniteCursorOptions<T> = {
  fetchPage: FetchPage<T>;
  mergeItems?: (previous: T[], next: T[]) => T[];
  initialCursor?: string | null;
};

export function useInfiniteCursor<T>({
  fetchPage,
  mergeItems,
  initialCursor = null,
}: UseInfiniteCursorOptions<T>) {
  const [items, setItems] = useState<T[]>([]);
  const [cursor, setCursor] = useState<string | null>(initialCursor);
  const [status, setStatus] = useState<InfiniteStatus>("idle");
  const [error, setError] = useState<Error | null>(null);

  const abortRef = useRef<AbortController | null>(null);
  const observerRef = useRef<IntersectionObserver | null>(null);
  const loadingRef = useRef(false);
  const hasMore = cursor !== null || items.length === 0;

  const loadMore = useCallback(async () => {
    if (loadingRef.current || !hasMore) return;

    loadingRef.current = true;
    abortRef.current?.abort();

    const controller = new AbortController();
    abortRef.current = controller;
    setStatus("loading");
    setError(null);

    try {
      const page = await fetchPage({ cursor, signal: controller.signal });
      setItems((previous) =>
        mergeItems ? mergeItems(previous, page.items) : [...previous, ...page.items],
      );
      setCursor(page.nextCursor);
      setStatus(page.nextCursor ? "idle" : "done");
    } catch (unknownError) {
      if (unknownError instanceof DOMException && unknownError.name === "AbortError") {
        return;
      }
      setError(unknownError instanceof Error ? unknownError : new Error("Load failed"));
      setStatus("error");
    } finally {
      loadingRef.current = false;
    }
  }, [cursor, fetchPage, hasMore, mergeItems]);

  const sentinelRef = useCallback(
    (node: HTMLElement | null) => {
      observerRef.current?.disconnect();
      if (!node || !hasMore) return;

      observerRef.current = new IntersectionObserver(
        ([entry]) => {
          if (entry?.isIntersecting) void loadMore();
        },
        { rootMargin: "600px 0px", threshold: 0 },
      );
      observerRef.current.observe(node);
    },
    [hasMore, loadMore],
  );

  useEffect(() => {
    void loadMore();
    return () => {
      abortRef.current?.abort();
      observerRef.current?.disconnect();
    };
  }, [loadMore]);

  return { items, status, error, hasMore, loadMore, sentinelRef };
}

Pour les effets et leur nettoyage, utilisez la référence officielle React useEffect. Demandez aussi à Claude Code de vérifier explicitement le cleanup de l’observer et des requêtes.

Composant De Liste

Le composant garde une issue manuelle. Si Intersection Observer échoue, si un navigateur d’entreprise bloque un comportement ou si l’utilisateur navigue au clavier, le bouton appelle le même loadMore.

import { useCallback } from "react";
import { useInfiniteCursor, type CursorPage } from "./useInfiniteCursor";

type Article = {
  id: string;
  title: string;
  summary: string;
  href: string;
  publishedAt: string;
};

function mergeUniqueById(previous: Article[], next: Article[]) {
  const seen = new Set(previous.map((item) => item.id));
  return [...previous, ...next.filter((item) => !seen.has(item.id))];
}

async function fetchArticlePage({
  cursor,
  signal,
}: {
  cursor: string | null;
  signal: AbortSignal;
}): Promise<CursorPage<Article>> {
  const params = new URLSearchParams({ limit: "20" });
  if (cursor) params.set("cursor", cursor);

  const response = await fetch(`/api/articles?${params}`, { signal });
  if (!response.ok) throw new Error(`Failed to load articles: ${response.status}`);
  return response.json();
}

export function ArticleFeed() {
  const fetchPage = useCallback(fetchArticlePage, []);
  const { items, status, error, hasMore, loadMore, sentinelRef } = useInfiniteCursor({
    fetchPage,
    mergeItems: mergeUniqueById,
  });

  return (
    <section aria-labelledby="article-feed-title">
      <h2 id="article-feed-title">Articles récents</h2>

      <div role="feed" aria-busy={status === "loading"}>
        {items.map((article, index) => (
          <article
            key={article.id}
            role="article"
            aria-posinset={index + 1}
            aria-setsize={hasMore ? -1 : items.length}
          >
            <a href={article.href}>
              <h3>{article.title}</h3>
            </a>
            <p>{article.summary}</p>
            <time dateTime={article.publishedAt}>
              {new Intl.DateTimeFormat("fr-FR").format(new Date(article.publishedAt))}
            </time>
          </article>
        ))}
      </div>

      {error && <p role="alert">Le chargement a échoué. Vérifiez la connexion puis réessayez.</p>}

      <div ref={sentinelRef} aria-hidden="true" />

      <p aria-live="polite">
        {status === "loading" && "Chargement d'autres articles."}
        {status === "done" && "Tous les articles sont affichés."}
      </p>

      {hasMore && (
        <button type="button" onClick={() => void loadMore()} disabled={status === "loading"}>
          Charger plus
        </button>
      )}
    </section>
  );
}

Si vous utilisez role="feed", lisez le pattern WAI-ARIA feed. Il aide à vérifier si la position, le chargement et l’erreur restent compréhensibles sans support visuel.

API Cursor Next.js

Le frontend ne compense pas une API instable. Cette route prend limit + 1 lignes, renvoie seulement limit éléments et utilise la ligne supplémentaire pour décider s’il existe une page suivante.

import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const limit = Math.min(Math.max(Number(searchParams.get("limit") ?? "20"), 1), 50);
  const cursor = searchParams.get("cursor");

  const rows = await prisma.article.findMany({
    take: limit + 1,
    ...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}),
    orderBy: [{ publishedAt: "desc" }, { id: "desc" }],
    select: {
      id: true,
      title: true,
      summary: true,
      href: true,
      publishedAt: true,
    },
  });

  const items = rows.slice(0, limit);
  const nextCursor = rows.length > limit ? items.at(-1)?.id ?? null : null;

  return NextResponse.json({ items, nextCursor });
}

En production, vérifiez aussi l’index de base de données associé à l’ordre publishedAt puis id. Un observer parfait ne rendra pas l’interface fluide si la requête se dégrade. Utilisez le même réflexe que dans l’optimisation de performance.

Pièges Concrets

Le premier piège est le déclenchement répété de l’observer. Si le sentinel reste visible pendant un rendu lent, une nouvelle requête peut partir avant la mise à jour du state. Un verrou immédiat via ref est plus sûr.

Le deuxième piège est l’offset pagination sur un flux vivant. Un nouvel article inséré en haut peut faire chevaucher les pages. Utilisez un cursor et dédupliquez côté client par id stable.

Le troisième piège est le footer inaccessible. Si le scroll infini repousse sans cesse les liens de contact, mentions légales ou formation Claude Code, la fonctionnalité nuit à la conversion. Après quelques pages, basculez vers un bouton manuel ou laissez apparaître le footer.

Le quatrième piège est le SEO. Les moteurs de recherche et aperçus sociaux ne dépendent pas de l’état de scroll. Gardez les liens normaux, pages de catégorie, sitemap et métadonnées.

Le cinquième piège est le bouton retour. Après avoir ouvert un détail, revenir en haut de la liste est frustrant. Testez la restauration de scroll, le cache et l’état des filtres dans l’URL.

Prompt De Revue Claude Code

Après l’implémentation, demandez une revue orientée risques.

Relis cette implémentation de scroll infini pour les risques de production.
Vérifie fetchs en double, réponses obsolètes, cleanup IntersectionObserver,
gestion AbortError, pagination cursor, accessibilité, SEO, bouton retour,
index de base de données et récupération manuelle après échec.
Retourne les problèmes par fichier avec des corrections concrètes.

Pour le contexte outil, partez de Anthropic Claude Code overview. Plus vous déléguez à un agent, plus les contraintes et la grille de revue deviennent importantes.

Résumé Et CTA

Le scroll infini semble être une petite amélioration UX, mais il touche navigateur, API, base de données, accessibilité, SEO et conversion. Avec Claude Code, demandez le flux complet : Intersection Observer, API cursor, récupération manuelle, cleanup, restauration de position et vérification.

Si votre équipe veut rendre cette qualité répétable, commencez par la formation Claude Code. L’objectif n’est pas seulement de générer un hook, mais d’apprendre à spécifier, relire, tester et publier ce type de fonctionnalité.

Résultat Vérifié

Pour cette mise à jour, j’ai vérifié les documentations MDN, React, WAI-ARIA et Anthropic, puis remplacé le contenu corrompu par un guide orienté production. Le code est structuré en TypeScript/TSX valide et couvre la protection contre les doublons, AbortController, l’API cursor, la récupération manuelle et aria-live. Dans un projet réel, je terminerais par npm run build, des tests de charge API, un test mobile et une vérification du retour navigateur.

#Claude Code #scroll infini #React #performance #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.