😸

useInfiniteQueryで簡単に無限スクロールを実装する

2023/12/23に公開

CastingONE Advent Calendar 2023 23 日目の記事です!

はじめに

こんにちは、CastingONE開発部です。

本日は、使ってみて非常に便利だった useInfiniteQuery について紹介したいと思います!
useInfiniteQueryTanstack Query が提供してる機能の一つで、「無限スクロール」を簡単に実装することができます。

無限スクロール

無限スクロールとは、一般的なUIによくあるパターンのことで、追加で読み込める情報がある場合に、スクロールイベント、あるいは追加読み込みボタンのクリックイベントによって、追加データをフェッチし、追加データを表示します。

ページネーションと大きく違うところは、基本的にユーザーはスクロールのみで追加情報を読み込むことができるため、シームレスでスムーズな体験を提供することが可能です。

無限スクロールは広く利用されており、身近なところで例を挙げるとSlackにも実装されています。以下は、無限スクロール処理に関係しているところだけ抽出していますが、レスポンスとして has_moreresponse_metadata 等の、追加で読み込めるデータがあるかという情報を受け取っていることがわかります。

{
  has_more: true,
  messages: [{ // メッセージ内容 }],
  response_metadata: {
    next_cursor: "bmV4dF20cosinaiNfjiUjiOnTJS"
  }
}

useInfiniteQuery

useInfiniteQuery は、Tanstack Queryで提供してくれる便利なhooksの一つで、無限スクロールのようなデータ取得のやり方をサポートしてくれています。
イメージとしては以下の流れで、追加読み込みを実装することができます。

  1. useInfiniteQuery を呼び、最初のデータを受け取る
  2. getNextPageParam で次のデータを取得するためのパラメータを返却する
  3. fetchNextPage を呼び、次のデータを取得する

上記の例は次のデータのみを取得する流れでしたが、中間のデータを取得した(前にも後ろにもスクロールできる)場合も fetchPreviousPagegetPreviousPageParam を使って実装することができます。

useInfiniteQuery を利用する上で重要なのは、getNextPageParam です。この関数の返り値によって、hasNextPage フラグが規定されるからです。getNextPageParamundefined 以外を返す場合、hasNextPage は常に true になります。そのため、追加処理を止めたい場合は、明示的に undefined を返却するロジックを実装する必要があります。

実装してみる

実際に実装してみたサンプルはこちらです。
余談ですが、IntersectionObserver に関する処理は、react-intersection-observeruseInViewを使うのがお手軽に扱えて便利です。

https://codesandbox.io/p/github/t-ham752/useinfinitequery-example/main?import=true&embed=1&file=%2Fsrc%2Fpages%2Findex.tsx

index.tsx
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 を使う箇所は以下のようになっています。

index.ts
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,
  });
};

最後に

弊社ではメンバーを大募集しています。
カジュアル面談もやってますので、お気軽にご連絡ください!

https://www.wantedly.com/projects/1130967
https://www.wantedly.com/projects/768663

明日のAdvent Calendar

明日のAdvent CalendarはCTOによる2023開発振り返り記事です!ぜひご覧ください!
昨年の振り返り記事↓
https://zenn.dev/castingone_dev/articles/3c2dc2bfcc10bb

Discussion