【React + Contextで無限スクロール付きスレッド一覧を実装してみた】

Reactアプリで「スレッド一覧」を表示し、スクロールが一番下に到達したときに次のスレッドを自動取得する「無限スクロール」を実装する。データはバックエンドAPIから offset と limit を使ってページごとに取得する構成。

```
// //ここでthreadsの情報データベース内の情報を保持
import { createContext, useState, useEffect } from "react";

// コンテキストを作成
export const ThreadsContext = createContext();

export const ThreadsProvider = ({ children }) => {
  const [getthreads, setGetThreads] = useState([]);//スレッドの一覧データ。初期値は空配列。
  const [offset, setOffset] = useState(0);//ページネーションの開始位置。初期値は 0。
  const [isLoading, setIsLoading] = useState(false);//現在データ取得中かどうかを管理するためのフラグ。多重呼び出し防止にも。
  const [hasMore, setHasMore] = useState(true); // ← 終端検知用
  const limit = 3;//一度に取得したいスレッドの件数。

  const fetchThreads = async (offset = 0, limit = 3) => {//非同期でスレッドを取得する関数。引数で offset/limit を受け取る。
    try {
      const response = await fetch(
        `http://localhost:8080/app/threads?offset=${offset}&limit=${limit}`,
        {
          method: "GET",
          headers: {
            Authorization: `Bearer ${localStorage.getItem("jwtToken")}`,
            "Content-Type": "application/json",
          },
        }
      );

      if (!response.ok) {
        throw new Error("スレッド一覧の取得に失敗しました");
      }

      const data = await response.json();
      console.log("取得したスレッド一覧:", data);

      //dataがない場合（投稿がもうない場合）
      if (data.length === 0) {
        setHasMore(false); // これ以上読み込まない
      } else {
        setGetThreads((prev) => {//すでに取得済みの ID を除外してから新しいスレッドを追加。重複を防ぐ。
          const existingIds = new Set(prev.map((t) => t.id));
          const deduped = data.filter((t) => !existingIds.has(t.id));
          return [...prev, ...deduped];
        });
      }
    } catch (error) {
      console.error("エラー:", error);
    }
  };

  //データ取得トリガー
  //offset が変わるたびにスレッドを読み込む。isLoading と hasMore を使って不要な呼び出しを防止。
  useEffect(() => {
    const loadThreads = async () => {
      if (!isLoading && hasMore) {
        setIsLoading(true);
        await fetchThreads(offset, limit);
        setIsLoading(false);
      }
    };
    loadThreads();
  }, [offset]);


  //スクロール検知
  useEffect(() => {
    const handleScroll = () => {
      const scrollThreshold = 100;
      const scrolledToBottom =
        window.innerHeight + document.documentElement.scrollTop >=
        document.documentElement.offsetHeight - scrollThreshold;

      if (scrolledToBottom && !isLoading && hasMore) {
        //画面の一番下に近づいたことを検知する
        console.log("一番下！");
        setOffset((prev) => prev + limit);
      }
    };

    window.addEventListener("scroll", handleScroll);
    //スクロールが条件を満たしたら offset を更新し、次のスレッドをロードする準備。
    return () => window.removeEventListener("scroll", handleScroll);
  }, [isLoading, hasMore]); // ← フラグを依存に

  return (
    <ThreadsContext.Provider value={{ getthreads, fetchThreads }}>
      {children}
    </ThreadsContext.Provider>
  );
};

```


・offset / limit	ページネーションの起点と件数。API に渡して部分取得を実現。
・isLoading	API 多重呼び出しを防ぐためのフラグ。
・hasMore	API が空データを返したとき、それ以上呼ばせないようにするための終端チェック。
・Set による重複除去	同じスレッド ID を2回表示させないために、取得済みの ID と照合してフィルタリング。


#### バックエンド側の設定

```
 public List<ThreadListDto> getThreads(Integer offset, Integer limit) {//外部から offset と limit を受け取り、スレッド一覧のDTOをリストとして返すメソッド。
        Pageable pageable = PageRequest.of(offset / limit, limit, Sort.by(Sort.Direction.DESC, "postedAt"));
        return threadsRepository.getThreads(pageable);
        //getThreadsは、スレッドリポジトリのfindThreadsの中身を返す
    }

```

こちらがサービスクラスの中身です！
PageRequest.of(page, size) は Spring Data のページングの仕組み。

ここで offset / limit をしているのは、クライアントが送ってくる「何件目から」を、Springが要求する「ページ番号」に変換しているため（ページ数 = offset ÷ limit）。

第3引数で postedAt の降順（新しい順）にソートしてる。
 例：offset=6, limit=3 なら PageRequest.of(2, 3) → 3件ずつの3ページ目（7〜9件目）を取得。


簡単にではありますが、備忘録として記載しておきｍ