Tips & Tricks (Diperbarui: 2/6/2026)

Infinite Scroll Siap Produksi dengan Claude Code dan React

Bangun infinite scroll dengan Claude Code, React, cursor API, aksesibilitas, SEO, dan penanganan gagal.

Infinite Scroll Siap Produksi dengan Claude Code dan React

Infinite scroll adalah pola UI yang memuat item berikutnya saat pengguna mendekati akhir daftar. Di feed sosial, arsip artikel, katalog produk, pusat notifikasi, dan log admin, pola ini terasa wajar. Namun versi produksi bukan sekadar “fetch saat kartu terakhir terlihat”.

Masalah yang sering muncul adalah request ganda, response lama menimpa state baru, posisi scroll hilang saat kembali dari halaman detail, pembaca layar tidak mendapat status yang jelas, SEO melemah, dan API offset mengulang atau melewatkan data saat record baru masuk. Jika kamu hanya meminta Claude Code “tambahkan infinite scroll”, hasilnya mungkin demo yang terlihat bagus, tetapi belum aman untuk rilis.

Artikel ini menjadikan pekerjaan tersebut sebagai workflow yang bisa diminta dan direview: prompt Claude Code, React hook, komponen feed, route Next.js berbasis cursor, beberapa use case nyata, jebakan konkret, link resmi, dan catatan verifikasi. Untuk daftar sangat besar yang masalah utamanya jumlah node DOM, baca virtual scroll. Jika pengguna perlu nomor halaman eksplisit, bandingkan dengan pagination implementation.

Tetapkan Desain Dulu

Intersection Observer adalah API browser yang memberi tahu kapan sebuah elemen target beririsan dengan viewport atau root container. Sederhananya, browser memberi sinyal ketika penanda di akhir daftar mulai mendekati layar. Referensi dasarnya adalah MDN Intersection Observer API.

Elemen kecil di akhir daftar itu sering disebut sentinel. Saat sentinel terlihat, aplikasi memuat halaman berikutnya. Ini lebih ringan daripada menghitung posisi pada setiap event scroll. Dengan rootMargin, request bisa dimulai sebelum pengguna benar-benar sampai di bawah.

Keputusan kedua adalah jenis pagination. Offset pagination meminta “lewati 40 item, ambil 20 berikutnya”. Ini cukup untuk daftar statis, tetapi feed yang berubah bisa membuat item dobel atau hilang. Cursor pagination meminta “lanjut setelah id ini”, sehingga lebih stabil untuk artikel, notifikasi, audit log, dan hasil pencarian yang bergerak.

Prompt yang lebih aman untuk Claude Code:

Implementasikan infinite scroll untuk daftar artikel di React dan Next.js.
Gunakan Intersection Observer dan API berbasis cursor.
Sertakan pencegahan request ganda, cleanup AbortController, pesan error terlihat,
tombol manual "Muat lagi", aria-live, role="feed", dan link normal yang aman untuk SEO.
Jangan hapus frontmatter, heroImage, internal link, atau route lokal.

Claude Code common workflows menekankan tugas yang jelas, contoh, dan batasan. Infinite scroll membutuhkan itu karena fiturnya menyentuh UI, API, aksesibilitas, dan alur produk.

Use Case Nyata

Use case pertama adalah arsip artikel. Situs tutorial dapat menjaga halaman awal tetap ringan sambil membiarkan pembaca yang tertarik terus menemukan artikel berikutnya. Risiko utamanya adalah navigasi balik: jika pembaca membuka detail lalu kembali dan posisinya hilang, pengalaman terasa rusak.

Use case kedua adalah pencarian ecommerce atau SaaS. Pengguna yang menelusuri produk, template, atau integrasi sering lebih nyaman dengan scrolling terus-menerus. Namun filter, urutan, dan query harus tetap ada di URL agar hasil bisa dibagikan.

Use case ketiga adalah pusat notifikasi dan audit log. Operator biasanya memeriksa data terbaru terlebih dahulu. Di sini cursor, timestamp, dan status sudah dibaca harus dipisah. Jangan memakai “terakhir terlihat” sebagai cursor teknis sekaligus status bisnis.

Use case keempat adalah chat, komentar, dan activity stream. Banyak layar membutuhkan reverse infinite scroll, yaitu memuat pesan lama di atas posisi saat ini. Beri tahu Claude Code arah loading secara eksplisit.

Use case kelima adalah learning dashboard. Pelajaran, contoh, dan checklist bisa mengalir dalam satu pengalaman, tetapi setiap bagian tetap membutuhkan URL stabil, progress marker, dan CTA seperti Claude Code training.

React Hook

Hook berikut mengasumsikan API berbasis cursor. Ia membatalkan pekerjaan lama dengan AbortController, mencegah fetch ganda dengan loadingRef, dan melakukan preload dengan 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 };
}

Untuk sinkronisasi dengan sistem eksternal dan cleanup, gunakan referensi resmi React useEffect. Saat meminta review ke Claude Code, sebutkan cleanup observer dan fetch secara eksplisit.

Komponen Feed

Komponen sebaiknya tidak hanya bergantung pada auto-load. Jika Intersection Observer gagal, browser perusahaan memblokir fitur, atau pengguna memakai keyboard, tombol manual tetap memanggil loadMore yang sama.

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">Artikel terbaru</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("id-ID").format(new Date(article.publishedAt))}
            </time>
          </article>
        ))}
      </div>

      {error && <p role="alert">Gagal memuat. Periksa koneksi lalu coba lagi.</p>}

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

      <p aria-live="polite">
        {status === "loading" && "Memuat artikel berikutnya."}
        {status === "done" && "Semua artikel sudah ditampilkan."}
      </p>

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

Jika memakai role="feed", lihat WAI-ARIA feed pattern. Tidak semua daftar memerlukannya, tetapi pola ini membantu memastikan posisi, loading, dan error tetap jelas tanpa animasi visual.

API Cursor Di Next.js

Frontend tidak bisa memperbaiki API yang tidak stabil. Route ini mengambil limit + 1 baris, mengembalikan hanya limit, lalu memakai baris ekstra untuk menentukan apakah ada cursor berikutnya.

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

Di produksi, pastikan index database sesuai dengan urutan publishedAt dan id. Jika query melambat, UI akan terasa rusak meski observer benar. Perlakukan ini seperti bagian dari optimasi performa.

Jebakan Umum

Jebakan pertama adalah observer terpicu berulang. Jika sentinel tetap terlihat saat render lambat, request baru bisa mulai sebelum state berubah. Ref lock lebih aman daripada hanya mengandalkan state.

Jebakan kedua adalah offset pagination pada feed yang berubah. Artikel baru di atas dapat membuat halaman kedua berisi item yang sudah muncul. Gunakan cursor dan dedupe berdasarkan id stabil.

Jebakan ketiga adalah footer dan CTA tidak pernah tercapai. Infinite scroll bisa terus mendorong kontak, halaman legal, atau Claude Code training ke bawah. Setelah beberapa halaman, hentikan auto-load atau ganti ke tombol manual.

Jebakan keempat adalah SEO. Mesin pencari dan social preview tidak bergantung pada kondisi scroll pengguna. Pertahankan link normal, kategori, sitemap, dan metadata.

Jebakan kelima adalah tombol kembali. Jika pengguna membuka detail lalu kembali ke awal daftar, pengalaman browsing terputus. Uji scroll restoration, cache, dan state filter di URL.

Prompt Review Claude Code

Setelah implementasi, minta review berbasis mode gagal, bukan review gaya umum.

Review implementasi infinite scroll ini untuk risiko produksi.
Periksa fetch ganda, response usang, cleanup IntersectionObserver,
penanganan AbortError, cursor pagination, aksesibilitas, SEO, tombol kembali,
index database, dan pemulihan manual setelah gagal.
Kembalikan temuan per file dan perbaikan konkret.

Untuk konteks resmi tool, gunakan Anthropic Claude Code overview. Semakin banyak pekerjaan yang didelegasikan ke agen, semakin penting batasan dan checklist review.

Ringkasan Dan CTA

Infinite scroll bukan sekadar animasi kecil. Ia menyentuh browser, API, database, aksesibilitas, SEO, dan funnel. Dengan Claude Code, minta workflow lengkap: Intersection Observer, cursor API, fallback manual, cleanup, pemulihan posisi, dan verifikasi.

Jika tim ingin menjadikan kualitas ini kebiasaan, mulai dari Claude Code training. Tujuannya bukan hanya membuat hook sekali, tetapi membangun cara tim menulis spesifikasi, review, test, dan checklist rilis.

Hasil Yang Dicek

Pada pembaruan ini, saya memeriksa dokumentasi MDN, React, WAI-ARIA, dan Anthropic, lalu mengganti konten yang rusak dengan panduan produksi. Kode disusun sebagai TypeScript/TSX valid dan mencakup perlindungan request ganda, AbortController, cursor API, pemulihan manual, dan aria-live. Dalam proyek nyata, saya akan melanjutkan dengan npm run build, pengujian beban API, tes browser mobile, dan verifikasi tombol kembali.

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