Claude Code से production-ready infinite scroll बनाना
Claude Code, React और cursor API से infinite scroll बनाएं; accessibility, SEO और failure handling सहित।
Infinite scroll वह UI pattern है जिसमें user list के नीचे पहुंचने से पहले अगला data अपने आप load हो जाता है। Social feed, article archive, product search, notification center और admin log में यह बहुत natural लगता है। लेकिन production में यह सिर्फ “last card दिखे तो fetch कर दो” जितना सरल नहीं है।
असल दिक्कतें duplicate request, पुराना response नया state overwrite करना, detail page से वापस आने पर scroll position खोना, screen reader को loading state न मिलना, SEO कमजोर होना और offset pagination की वजह से item repeat या skip होना हैं। अगर आप Claude Code से सिर्फ “infinite scroll बना दो” कहेंगे, तो demo जल्दी बन सकता है, पर launch-ready implementation नहीं।
इस article में हम Claude Code prompt, React hook, feed component, Next.js cursor API, तीन से अधिक use cases, concrete pitfalls, official references और verification note को एक साथ देखेंगे। अगर समस्या बहुत बड़े DOM की है, तो virtual scroll guide पढ़ें। अगर user को page number चुनना जरूरी है, तो pagination implementation से तुलना करें।
पहले Design साफ करें
Intersection Observer browser API है जो बताती है कि कोई target element viewport या किसी container से intersect कर रहा है या नहीं। सरल भाषा में, browser हमें बताता है कि list के अंत में रखा छोटा marker screen के पास आ गया है। Behavior और terminology के लिए MDN Intersection Observer API सही reference है।
List के अंत में रखे marker को अक्सर sentinel कहा जाता है। इसे “निगरानी element” समझें। जब यह दिखता है, app अगला page load करता है। यह हर scroll event पर calculation करने से हल्का है। rootMargin की मदद से request थोड़ा पहले शुरू हो सकती है।
दूसरा design decision pagination है। Offset pagination कहता है “40 item skip करो और अगले 20 लाओ”। Live feed में नया data आने पर यह duplicate या missing item पैदा कर सकता है। Cursor pagination कहता है “इस id के बाद से जारी रखो”। Article feed, notification, audit log और changing search results में cursor ज्यादा stable रहता है।
Claude Code को prompt इस तरह दें:
React और Next.js में article list के लिए infinite scroll implement करें।
Intersection Observer इस्तेमाल करें और API cursor-based रखें।
Duplicate request prevention, AbortController cleanup, visible error state,
manual "Load more" button, aria-live, role="feed" और SEO-safe normal links शामिल करें।
Existing frontmatter, heroImage, internal links और localized routes न हटाएं।
Claude Code common workflows clear task, example और constraint देने पर जोर देते हैं। Infinite scroll में यह और जरूरी है, क्योंकि feature UI, API, accessibility और product journey को एक साथ छूता है।
Practical Use Cases
पहला use case article archive है। Tutorial site initial page हल्का रख सकती है और interested reader को आगे articles दिखा सकती है। लेकिन detail article खोलकर वापस आने पर अगर position खो जाए, तो reader को फिर से शुरुआत करनी पड़ती है।
दूसरा use case ecommerce या SaaS search है। Product, template या integration browse करते समय continuous scroll अच्छा लगता है। लेकिन filter, sort और query URL में रहने चाहिए ताकि वही result teammate को share किया जा सके।
तीसरा use case admin notification और audit log है। Operator latest entries पहले scan करते हैं। यहां cursor, timestamp और read status को अलग रखें। “Last seen” को database cursor और business state दोनों न बनाएं।
चौथा use case chat, comments और activity stream है। इनमें अक्सर reverse infinite scroll चाहिए होता है, यानी पुराने messages ऊपर load होते हैं। Claude Code को loading direction साफ लिखें, क्योंकि scroll restoration logic बदल जाता है।
पांचवां use case learning dashboard है। Lessons, code examples और checklists continuous flow में दिख सकते हैं, लेकिन हर section की stable URL, progress marker और Claude Code training जैसी CTA reachable रहनी चाहिए।
React Hook
यह hook cursor API मानकर बनाया गया है। AbortController पुराने request साफ करता है, loadingRef duplicate fetch रोकता है, और rootMargin bottom आने से पहले preload करता है।
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 };
}
Effect cleanup के लिए React useEffect official reference देखें। Claude Code से review कराते समय observer और fetch cleanup को explicitly mention करें।
Feed Component
Component में automatic loading के साथ manual recovery भी रखें। अगर Intersection Observer fail हो, browser policy block करे, या user keyboard से काम करे, तो वही loadMore button से भी चले।
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</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("hi-IN").format(new Date(article.publishedAt))}
</time>
</article>
))}
</div>
{error && <p role="alert">Load fail हुआ। Connection check करके फिर कोशिश करें।</p>}
<div ref={sentinelRef} aria-hidden="true" />
<p aria-live="polite">
{status === "loading" && "और articles load हो रहे हैं।"}
{status === "done" && "सभी articles दिखा दिए गए हैं।"}
</p>
{hasMore && (
<button type="button" onClick={() => void loadMore()} disabled={status === "loading"}>
और load करें
</button>
)}
</section>
);
}
अगर role="feed" इस्तेमाल करें, तो WAI-ARIA feed pattern देखें। हर simple list में यह जरूरी नहीं, लेकिन यह पूछने में मदद करता है कि screen reader user को position, loading और error समझ आ रहे हैं या नहीं।
Next.js Cursor API
Frontend unstable API को fix नहीं कर सकता। यह route limit + 1 rows लाता है, सिर्फ limit return करता है, और extra row से next cursor तय करता है।
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 });
}
Production में database index भी check करें। अगर query slow है, UI भी slow लगेगा, चाहे observer सही हो। इसे performance optimization की तरह API latency और query plan के साथ देखें।
Common Pitfalls
पहला pitfall repeated observer firing है। Sentinel screen में रहता है और render slow है, तो दूसरा request शुरू हो सकता है। इसलिए सिर्फ state नहीं, ref lock भी रखें।
दूसरा pitfall changing feed में offset pagination है। नया item ऊपर insert होते ही दूसरी page में duplicate आ सकता है। Cursor API और stable id से dedupe करें।
तीसरा pitfall footer और CTA तक न पहुंच पाना है। Infinite scroll contact, legal pages या Claude Code training को हमेशा नीचे धकेल सकता है। कुछ pages के बाद automatic loading रोकें या manual button दिखाएं।
चौथा pitfall SEO है। Search engine और social preview user के scroll state पर depend नहीं करते। Normal links, category URLs, sitemap और metadata रखें।
पांचवां pitfall browser back है। Detail page से वापस आकर अगर list top पर चली जाए, तो browsing flow टूटता है। Scroll restoration, cache और URL state test करें।
Claude Code Review Prompt
Implementation के बाद vague review न मांगें। Failure-mode review मांगें।
इस infinite scroll implementation को production risk के लिए review करें।
Duplicate fetch, stale response, IntersectionObserver cleanup,
AbortError handling, cursor pagination, accessibility, SEO, browser back,
database index और failure के बाद manual recovery check करें।
File-level findings और concrete fixes दें।
Tool की official context के लिए Anthropic Claude Code overview देखें। Agent को जितना ज्यादा काम देंगे, constraints और review checklist उतनी महत्वपूर्ण होंगी।
Summary और CTA
Infinite scroll छोटी UI improvement नहीं है। यह browser, API, database, accessibility, SEO और conversion path को जोड़ता है। Claude Code इस्तेमाल करते समय पूरा workflow मांगें: Intersection Observer, cursor API, manual fallback, cleanup, position restore और verification।
अगर आपकी team इसे repeatable quality में बदलना चाहती है, तो Claude Code training से शुरू करें। लक्ष्य सिर्फ एक hook generate करना नहीं, बल्कि specification, review, test और release checklist को team habit बनाना है।
मैंने क्या Verify किया
इस update में मैंने MDN, React, WAI-ARIA और Anthropic docs देखे और corrupted content को production-focused guide से बदला। Code valid TypeScript/TSX structure में है और duplicate request protection, AbortController, cursor API, manual recovery और aria-live cover करता है। Real project में इसके बाद npm run build, API load check, mobile browser test और browser back restoration verify करना चाहिए।
मुफ़्त PDF: Claude Code cheatsheet
Email डालें और commands, review habits तथा safe workflow वाली एक-page PDF पाएँ.
हम आपका data सुरक्षित रखते हैं और spam नहीं भेजते.
लेखक के बारे में
Masa
Claude Code workflow और team adoption पर काम करने वाला engineer.
संबंधित लेख
Claude Code permission safety ladder: access धीरे-धीरे बढ़ाएं
read-only से limited edits, proof commands और deploy checks तक permission बढ़ाने की सुरक्षित ladder.
Claude Code Small PR Proof Pack: छोटे PR को review-ready बनाना
Claude Code PR के लिए diff, checks, public URL, CTA path और rollback वाला practical proof pack.
Claude Code Review Gate Before Commit: diff, test, public URL और CTA जांच
Claude Code से commit से पहले review gate बनाएं: diff, build, public URL, Gumroad, consultation, tests और unrelated files।