Tips & Tricks (Aktualisiert: 2.6.2026)

Infinite Scroll mit Claude Code und React produktionsreif umsetzen

Baue Infinite Scroll mit Claude Code, React, Cursor-API, Accessibility, SEO und sauberem Fehlerpfad.

Infinite Scroll mit Claude Code und React produktionsreif umsetzen

Infinite Scroll lädt weitere Einträge, sobald Nutzerinnen und Nutzer dem Ende einer Liste nahekommen. In Social Feeds, Artikelarchiven, Produktkatalogen oder Admin-Logs wirkt das selbstverständlich. In Produktion ist es aber mehr als “fetch auslösen, wenn die letzte Karte sichtbar ist”.

Die typischen Fehler sind doppelte Requests, alte Antworten, die neuen Zustand überschreiben, verlorene Scrollposition nach dem Zurück-Navigieren, unklare Zustände für Screenreader, schwaches SEO und APIs, die bei neuen Datensätzen Elemente überspringen oder doppeln. Wenn du Claude Code nur “baue Infinite Scroll” sagst, entsteht schnell eine Demo, aber nicht unbedingt eine robuste Funktion.

Dieser Artikel macht daraus einen klaren Arbeitsauftrag: Prompt für Claude Code, React Hook, Feed-Komponente, Next.js Route mit Cursor, reale Use Cases, konkrete Fallstricke, offizielle Links und ein Prüfvermerk. Wenn die Hauptfrage die Menge der DOM-Knoten ist, lies zusätzlich Virtual Scroll. Wenn Nutzer explizit Seiten auswählen müssen, vergleiche mit Pagination.

Entscheidungen Vor Dem Code

Intersection Observer ist eine Browser-API, die meldet, wenn ein Zielelement den Viewport oder ein Root-Element schneidet. Einfach gesagt: Der Browser sagt dir, wann die Markierung am Listenende in Bildschirmnähe kommt. Die maßgebliche Referenz ist MDN Intersection Observer API.

Dieses kleine Element am Listenende wird oft Sentinel genannt. Wird es sichtbar, lädt die App die nächste Seite. Das ist leichter und stabiler als ein Scroll-Handler, der bei jeder Bewegung Positionen berechnet. Mit rootMargin kann der Request früher starten, bevor die Person wirklich am Ende angekommen ist.

Die zweite Entscheidung ist die Paginierung. Offset-Paginierung fragt nach “überspringe 40, nimm 20”. Das ist für statische Listen okay, aber bei lebenden Feeds verschieben neue Einträge die Seiten. Cursor-Paginierung fragt nach “weiter nach dieser id” und ist deshalb für Artikel, Benachrichtigungen, Audit Logs und Suchergebnisse meist besser.

Ein guter Claude Code Prompt lautet:

Implementiere einen Artikel-Feed mit Infinite Scroll in React und Next.js.
Nutze Intersection Observer und eine cursor-basierte API.
Baue Schutz vor doppelten Requests, AbortController-Cleanup, sichtbare Fehler,
einen manuellen "Mehr laden"-Button, aria-live, role="feed" und SEO-sichere Links ein.
Entferne kein frontmatter, heroImage, interne Links oder lokalisierte Routen.

Die Claude Code Common Workflows betonen klare Aufgaben, Beispiele und Grenzen. Bei Infinite Scroll ist das besonders wichtig, weil UI, API, Accessibility und Produktverhalten zusammenhängen.

Sinnvolle Use Cases

Der erste Use Case ist ein Artikelarchiv. Eine Tutorial-Seite kann die initiale Seite schnell halten und engagierten Leserinnen und Lesern weitere Inhalte anbieten. Der kritische Punkt ist die Rückkehr aus einem Detailartikel. Wenn die Position verloren geht, fühlt sich der Feed defekt an.

Der zweite Use Case ist Suche in Ecommerce oder SaaS. Produkte, Vorlagen oder Integrationen lassen sich flüssig durchstöbern. Filter, Sortierung und Suchbegriff müssen aber in der URL bleiben, sonst kann niemand dieselbe Ansicht teilen oder später wieder öffnen.

Der dritte Use Case sind Benachrichtigungen und Audit Logs. Operative Teams prüfen meist die neuesten Einträge zuerst. Cursor, Zeitstempel und Gelesen-Status müssen getrennt bleiben. Verwende den “zuletzt gesehenen” Eintrag nicht gleichzeitig als technischen Cursor und Business-Status.

Der vierte Use Case sind Chat, Kommentare und Activity Streams. Häufig braucht man Reverse Infinite Scroll, also ältere Einträge oberhalb der aktuellen Position. Sag Claude Code ausdrücklich, in welche Richtung geladen wird.

Der fünfte Use Case ist ein Lern-Dashboard. Lektionen, Beispiele und Checklisten können fortlaufend erscheinen, aber stabile URLs, Fortschritt und eine CTA wie Claude Code Training müssen erreichbar bleiben.

React Hook

Der folgende Hook geht von einer Cursor-API aus. Er bricht alte Requests mit AbortController ab, verhindert doppelte Fetches mit loadingRef und lädt durch rootMargin vor, bevor das Ende sichtbar wird.

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

Für Effekte und Cleanup ist die offizielle React useEffect Referenz der richtige Ausgangspunkt. Bitte Claude Code bei der Review ausdrücklich um Prüfung von Observer- und Request-Cleanup.

Feed-Komponente

Die Komponente behält einen manuellen Ausweg. Wenn Intersection Observer nicht greift, ein Enterprise-Browser blockiert oder jemand per Tastatur navigiert, nutzt der Button denselben loadMore-Pfad.

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">Neue Artikel</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("de-DE").format(new Date(article.publishedAt))}
            </time>
          </article>
        ))}
      </div>

      {error && <p role="alert">Laden fehlgeschlagen. Prüfe die Verbindung und versuche es erneut.</p>}

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

      <p aria-live="polite">
        {status === "loading" && "Weitere Artikel werden geladen."}
        {status === "done" && "Alle Artikel sind sichtbar."}
      </p>

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

Wenn du role="feed" nutzt, prüfe das WAI-ARIA Feed Pattern. Es hilft zu klären, ob Position, Ladezustand und Fehler auch ohne visuelle Animation verständlich sind.

Next.js Cursor API

Das Frontend kann keine instabile API reparieren. Diese Route holt limit + 1 Zeilen, gibt nur limit zurück und nutzt die zusätzliche Zeile, um einen nächsten Cursor zu bestimmen.

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

In Produktion muss der Datenbankindex zur Sortierung passen. Eine langsame Query macht die Oberfläche träge, auch wenn die Browserlogik korrekt ist. Denke hier wie bei Performance-Optimierung: API-Latenz und Ausführungsplan gehören zur UI-Qualität.

Häufige Fehler

Der erste Fehler ist mehrfaches Auslösen des Observers. Wenn der Sentinel sichtbar bleibt, kann ein weiterer Request starten, bevor React-State aktualisiert ist. Ein Ref-Lock ist dafür robuster.

Der zweite Fehler ist Offset-Paginierung in einem lebenden Feed. Ein neuer Artikel oben kann Seite zwei mit Seite eins überlappen lassen. Nutze Cursor und dedupliziere clientseitig per stabiler id.

Der dritte Fehler ist ein unerreichbarer Footer. Wenn Infinite Scroll Kontakt, Impressum oder Claude Code Training endlos nach unten schiebt, leidet die Conversion. Stoppe automatisches Laden nach einigen Seiten oder wechsle zu einem manuellen Button.

Der vierte Fehler ist SEO. Crawler und Social Previews können keinen Scrollzustand voraussetzen. Normale Links, Kategorien, Sitemap und Metadaten müssen erhalten bleiben.

Der fünfte Fehler ist die Zurück-Navigation. Wer aus einem Detailartikel zurückkehrt, sollte nicht wieder ganz oben landen. Teste Scroll Restoration, Cache und URL-Zustand für Filter und Cursor.

Review Prompt Für Claude Code

Nach der Umsetzung lohnt sich eine Risikoreview statt einer allgemeinen Stilprüfung.

Reviewe diese Infinite-Scroll-Implementierung für Produktionsrisiken.
Prüfe doppelte Fetches, veraltete Antworten, IntersectionObserver-Cleanup,
AbortError-Handling, Cursor-Paginierung, Accessibility, SEO, Browser-Zurück,
Datenbankindizes und manuelle Wiederherstellung nach Fehlern.
Gib Befunde pro Datei mit konkreten Fixes zurück.

Den offiziellen Einstieg in das Tool bietet Anthropic Claude Code overview. Je mehr Arbeit du einem Agenten gibst, desto wichtiger sind präzise Grenzen und Review-Kriterien.

Fazit Und CTA

Infinite Scroll ist keine kleine Animation, sondern eine Funktion über Browser, API, Datenbank, Accessibility, SEO und Funnel hinweg. Mit Claude Code solltest du den ganzen Ablauf anfordern: Intersection Observer, Cursor API, manueller Fallback, Cleanup, Positionswiederherstellung und Prüfung.

Wenn dein Team diese Qualität wiederholbar machen will, starte mit Claude Code Training. Ziel ist nicht, einmal einen Hook zu erzeugen, sondern Spezifikation, Review, Tests und Release-Checks als Teamroutine aufzubauen.

Geprüftes Ergebnis

Für dieses Update habe ich MDN, React, WAI-ARIA und Anthropic Dokumentation geprüft und den beschädigten Artikel durch eine produktionsnahe Anleitung ersetzt. Der Code ist als gültiges TypeScript/TSX strukturiert und deckt Duplicate-Request-Schutz, AbortController, Cursor API, manuellen Fallback und aria-live ab. In einem echten Projekt würde ich anschließend npm run build, API-Lasttests, Mobilbrowser und Zurück-Navigation prüfen.

#Claude Code #Infinite Scroll #React #Performance #UX
Kostenlos

Kostenloses PDF: Claude-Code-Cheatsheet

E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.

Wir schützen Ihre Daten und senden keinen Spam.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.