Tips & Tricks

Como implementar scroll infinito con Claude Code

Aprenda a implementar scroll infinito usando Claude Code. Incluye ejemplos practicos de codigo y guia paso a paso.

無限スクロールの仕組み

無限スクロールは、ユーザーがページ末尾に近づくと自動的に次のデータを読み込むUIパターンです。SNSやニュースアプリでおなじみですが、パフォーマンスやアクセシビリティを考慮した実装は意外と難しいものです。Claude Codeなら、ベストプラクティスに沿った実装を素早く構築できます。

Intersection Observerを使った基本実装

> Intersection Observerを使った無限スクロールのReactフックを作って。
> ローディング状態とエラーハンドリングも含めて。
import { useEffect, useRef, useState, useCallback } from 'react';

interface UseInfiniteScrollOptions<T> {
  fetchFn: (page: number) => Promise<{ data: T[]; hasMore: boolean }>;
  initialPage?: number;
  threshold?: number;
}

function useInfiniteScroll<T>({ fetchFn, initialPage = 1, threshold = 0.8 }: UseInfiniteScrollOptions<T>) {
  const [items, setItems] = useState<T[]>([]);
  const [page, setPage] = useState(initialPage);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  const observerRef = useRef<IntersectionObserver | null>(null);

  const lastElementRef = useCallback(
    (node: HTMLElement | null) => {
      if (loading) return;
      if (observerRef.current) observerRef.current.disconnect();

      observerRef.current = new IntersectionObserver(
        (entries) => {
          if (entries[0].isIntersecting && hasMore) {
            setPage((prev) => prev + 1);
          }
        },
        { threshold }
      );

      if (node) observerRef.current.observe(node);
    },
    [loading, hasMore, threshold]
  );

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    setError(null);

    fetchFn(page)
      .then((result) => {
        if (!cancelled) {
          setItems((prev) => [...prev, ...result.data]);
          setHasMore(result.hasMore);
        }
      })
      .catch((err) => { if (!cancelled) setError(err); })
      .finally(() => { if (!cancelled) setLoading(false); });

    return () => { cancelled = true; };
  }, [page, fetchFn]);

  return { items, loading, hasMore, error, lastElementRef };
}

コンポーネントでの使用例

function ArticleList() {
  const fetchArticles = useCallback(async (page: number) => {
    const res = await fetch(`/api/articles?page=${page}&limit=20`);
    const data = await res.json();
    return { data: data.articles, hasMore: data.hasMore };
  }, []);

  const { items, loading, hasMore, error, lastElementRef } = useInfiniteScroll({
    fetchFn: fetchArticles,
  });

  return (
    <div role="feed" aria-busy={loading} aria-label="記事一覧">
      {items.map((article, index) => (
        <article
          key={article.id}
          ref={index === items.length - 1 ? lastElementRef : null}
          aria-posinset={index + 1}
          aria-setsize={hasMore ? -1 : items.length}
        >
          <h2>{article.title}</h2>
          <p>{article.summary}</p>
        </article>
      ))}

      {loading && <div aria-live="polite">読み込み中...</div>}
      {error && <div role="alert">エラーが発生しました。再試行してください。</div>}
      {!hasMore && <p>すべての記事を表示しました。</p>}
    </div>
  );
}

バックエンドのカーソルベースページネーション

// オフセットベースよりもカーソルベースの方がパフォーマンスが良い
app.get('/api/articles', async (req, res) => {
  const limit = Math.min(Number(req.query.limit) || 20, 100);
  const cursor = req.query.cursor as string | undefined;

  const where = cursor ? { id: { gt: cursor } } : {};

  const articles = await prisma.article.findMany({
    where,
    take: limit + 1,
    orderBy: { id: 'asc' },
  });

  const hasMore = articles.length > limit;
  const data = hasMore ? articles.slice(0, -1) : articles;

  res.json({
    data,
    hasMore,
    nextCursor: hasMore ? data[data.length - 1].id : null,
  });
});

Summary

Claude Codeを使えば、Intersection Observerベースの無限スクロールからカーソルベースAPIまで一貫して構築できます。大量データの描画パフォーマンスには仮想スクロール実装を、ページネーション方式の比較はページネーション実装を参照してください。

Intersection Observer APIの詳細はMDN Web Docsをご覧ください。

#Claude Code #infinite scroll #React #performance #UX