TanStack Query入門:useInfiniteQueryで実現する快適な無限スクロール

(引用: https://tanstack.com/query/latest)

こんにちは。エンジニアのすぎやまです。

今回は、TanStack QueryのuseInfiniteQueryについて、実際に案件で使用して便利だったので記事にしました。

もともと、20件づつ表示するユーザーリストを作成していました。ユーザーのステータスを更新する際に、最初の20件はキャッシュが更新されるのですが、無限スクロールで読み込んでいった後ろのデータはうまくキャッシュの処理が行えておらず、更新前の古い値が表示されたままでした。

この時は、RTK Queryを使用してユーザー一覧の無限スクロールを作成していました。RTK QueryではInfinite Queryは公式にサポートされておらず、データを表示するだけの無限スクロールの作成は可能でしたが、更新が入ると作成が難しくなってしまいました。

この問題を解決するために調査した結果、TanStack Queryを使用して無限スクロールを作成することにしました。

TanStack Queryとは?

TanStack Query(旧React Query)はデータフェッチライブラリです。Web アプリケーションでのサーバー状態の取得、キャッシュ、同期、更新が非常に簡単になります。

RTK QueryやSWRと比較するとより多くの機能を備えており、細かく設定をすることが可能です。

またv4からはReact以外のVueなどのフレームワークでも使用可能となっています。

↓他の類似ライブラリとの比較はこちら

Comparison | React Query vs SWR vs Apollo vs RTK Query vs React Router

useInfiniteQueryで無限スクロール

今回は、TanStack Queryの機能の一つのInfinite Queriesについてご紹介します。無限スクロールの実装の際に使われると思います。

無限スクロールはページネーションとは違い、ユーザーがスクロールするだけで次の情報を読み込んで表示することができます。そのためユーザーの負荷が減り、ユーザーエクスペリエンスの向上に繋がります。

無限クエリをするには、 useInfiniteQuery() を使用します。

  const {
    data,
    error,
    fetchNextPage,
    hasNextPage,
    isFetching,
    isFetchingNextPage,
    status,
  } = useInfiniteQuery({
    queryKey: ['todo'],
    queryFn: ({pageParam}) => fetchTodos(pageParam),
    select: (data) => selectData(data)
    initialPageParam: 0,
    getNextPageParam: (lastPage, pages) => getNextTodos(lastPage.pager),
  })

以下の流れで追加の情報を取得していきます。

  1. useInfiniteQueryのqueryFnに設定したapiでデータの取得を行います。
  2. getNextPageParamで次のクエリの情報を返します。
  3. queryFnに設定した関数の引数にpageParamが渡されます。
  4. 再度データを取得し、次のデータを受け取ります。

getNextPageParamにundefinedを渡すと追加で取得する処理が止まり、無限スクロールの場合はスクロールはそこまでとなります。

またqueryFnにはデータを取得するAPI関数を渡し、queryKeyにはキャッシュのキーを設定しておきます。

selectでは取得したデータの形を整形して渡すことができます。maxPagesのオプションを記述することでページ数の制限をすることも可能です。

例えばデータの順を逆にし、3ページ分表示したい場合は以下のようなコードになります。

useInfiniteQuery({
  queryKey: ['projects'],
  queryFn: fetchProjects,
  select: (data) => ({
    pages: [...data.pages].reverse(),
    pageParams: [...data.pageParams].reverse(),
  }),
 maxPages: 3,
})

実装例

実際にユーザー一覧を作成した際には、以下のようにhookを作成しました。

selectではuserの情報に加えて、totalで何件あるのかの情報も追加して渡すようにしています。

検索やソートのために何か値を渡したい場合は、paramsという引数で値を受け取り、queryFnの関数で使用することができます。

export const userListQueryKey = 'user-list'
export const useUserList = ({ ...params }: Params) => {
  return useInfiniteQuery({
    queryKey: [userListQueryKey],
    queryFn: ({ pageParam }) => fetch(`http://hogeApiUrl?offset=${pageParam
}`),
    initialPageParam: 0,
    getNextPageParam: (lastPage) => getNextPage(lastPage.pager),
    select: (data) => {
      return {
        users: data?.pages?.reduce((acc, curr) => {
          acc.push(...curr.users);
          return acc;
        }, []),
        total: data.pages.at(0).pager.total,
      };
    },
    refetchOnWindowFocus: false,
  });
};

このhookを使用したいコンポーネントで呼び出します。

dataの中にユーザー一覧の情報が入っています。data以外にもローディング中などの値も受け取ることができます。

 const {
    data,
    error,
    fetchNextPage,
    isFetching,
    isFetchingNextPage,
    isLoading,
    hasNextPage,
    status
  } = useUsers({
    limit: 20,
  });

このdataをテーブルに渡せば表示できます。テーブルにはReact Virtuosoというライブラリを利用しました。

 return (
    <TableVirtuoso
      ref={ref}
      data={
        data?.length
          ? data
          : Array.from({ length: rowsPerPage }, () => ({} as T))
      }
      components={{
        Table,
        TableHead,
        TableRow,
        TableBody,
      }}
   />
)

これだけでユーザー一覧の無限スクロールを実装することができました。

削除や更新などデータに対して何か処理をする際にはuseMutationを使用します。

mutationFnにはAPIの処理を記述します。

onSuccessは成功時、onErrorは失敗時に行いたい処理を記述します。成功しても失敗しても行いたい処理は、onSettledに処理を書きます。

またPromiseの値を取得したいため、mutateではなくmutateAsyncをupdateUserという名前で受け取っています。

以下のコードの場合、成功時にキーをinvalidateさせ、最新のユーザー一覧情報を取得するようにしています。

const { mutateAsync: updateUser } = useMutation<
    UserResponse,
    Error,
    UserRequest
  >({
    mutationFn: (params) =>updateUser(params),
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: [userListQueryKey],
      });
    },
    onError: (error) => {
     console.log(error)
    },
  });

参考:Mutations

まとめ

useInfiniteQuery は、ユーザーがスクロールするたびに追加のデータを読み込む「無限スクロール」や「無限ページネーション」のような場面で特におすすめです。フィードの読み込みや検索結果のページネーションがシームレスに表示されていたら、このライブラリが使われているかもしれません。

機能によってはUIやその他の処理も諸々実装は必要かもしれませんが、無限スクロール自体はデータをテーブルに渡すだけで実装は簡単にできることが分かりました!

他にもたくさんの機能があり、非常に便利なので今後の案件でも活用していきたいです。

参考

TanStack Query 〜プロダクトで採用するための勘所〜

TanStack Queryは神フレームワーク

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