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件目)を取得。
簡単にではありますが、備忘録として記載しておきm