useInfiniteQueryで簡単に無限スクロールを実装する
CastingONE Advent Calendar 2023 23 日目の記事です!
はじめに
こんにちは、CastingONE開発部です。
本日は、使ってみて非常に便利だった useInfiniteQuery について紹介したいと思います!
useInfiniteQuery は Tanstack Query が提供してる機能の一つで、「無限スクロール」を簡単に実装することができます。
無限スクロール
無限スクロールとは、一般的なUIによくあるパターンのことで、追加で読み込める情報がある場合に、スクロールイベント、あるいは追加読み込みボタンのクリックイベントによって、追加データをフェッチし、追加データを表示します。
ページネーションと大きく違うところは、基本的にユーザーはスクロールのみで追加情報を読み込むことができるため、シームレスでスムーズな体験を提供することが可能です。
無限スクロールは広く利用されており、身近なところで例を挙げるとSlackにも実装されています。以下は、無限スクロール処理に関係しているところだけ抽出していますが、レスポンスとして has_more
や response_metadata
等の、追加で読み込めるデータがあるかという情報を受け取っていることがわかります。
{
has_more: true,
messages: [{ // メッセージ内容 }],
response_metadata: {
next_cursor: "bmV4dF20cosinaiNfjiUjiOnTJS"
}
}
useInfiniteQuery
useInfiniteQuery は、Tanstack Queryで提供してくれる便利なhooksの一つで、無限スクロールのようなデータ取得のやり方をサポートしてくれています。
イメージとしては以下の流れで、追加読み込みを実装することができます。
-
useInfiniteQuery
を呼び、最初のデータを受け取る -
getNextPageParam
で次のデータを取得するためのパラメータを返却する -
fetchNextPage
を呼び、次のデータを取得する
上記の例は次のデータのみを取得する流れでしたが、中間のデータを取得した(前にも後ろにもスクロールできる)場合も fetchPreviousPage
や getPreviousPageParam
を使って実装することができます。
useInfiniteQuery
を利用する上で重要なのは、getNextPageParam
です。この関数の返り値によって、hasNextPage
フラグが規定されるからです。getNextPageParam
が undefined
以外を返す場合、hasNextPage
は常に true
になります。そのため、追加処理を止めたい場合は、明示的に undefined
を返却するロジックを実装する必要があります。
実装してみる
実際に実装してみたサンプルはこちらです。
余談ですが、IntersectionObserver に関する処理は、react-intersection-observer の useInView
を使うのがお手軽に扱えて便利です。
import { useInfiniteDataQuery } from "@/api";
import { useInView } from "react-intersection-observer";
import { useEffect } from "react";
import { Card } from "@/components";
export default function Home() {
const { data, isLoading, hasNextPage, fetchNextPage, isFetchingNextPage } =
useInfiniteDataQuery();
const { ref, inView } = useInView();
useEffect(() => {
if (hasNextPage && inView) {
fetchNextPage();
}
}, [inView, hasNextPage, fetchNextPage]);
if (isLoading || !data) {
return <div>Loading...</div>;
}
const items = data.pages.map((page) => page.messages).flat();
return (
<div
style={{
maxHeight: "500px",
overflow: "auto",
}}
>
{items.map((item) => {
return <Card key={item.id} id={item.id} />;
})}
{isFetchingNextPage && <div>Loading...</div>}
<div style={{ visibility: "hidden", height: 0 }} ref={ref}>
<div />
</div>
</div>
);
}
useInfiniteQuery
を使う箇所は以下のようになっています。
export const useInfiniteDataQuery = () => {
return useInfiniteQuery({
queryKey: ["data"],
queryFn: ({ pageParam }) => fetchData(pageParam),
initialPageParam: 0,
getNextPageParam: (lastPage: APIResponse) =>
lastPage.meta.next_cursor != null ? lastPage.meta.next_cursor : undefined,
});
};
最後に
弊社ではメンバーを大募集しています。
カジュアル面談もやってますので、お気軽にご連絡ください!
明日のAdvent Calendar
明日のAdvent CalendarはCTOによる2023開発振り返り記事です!ぜひご覧ください!
昨年の振り返り記事↓
Discussion