(引用: 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),
})
以下の流れで追加の情報を取得していきます。
- useInfiniteQueryのqueryFnに設定したapiでデータの取得を行います。
- getNextPageParamで次のクエリの情報を返します。
- queryFnに設定した関数の引数にpageParamが渡されます。
- 再度データを取得し、次のデータを受け取ります。
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やその他の処理も諸々実装は必要かもしれませんが、無限スクロール自体はデータをテーブルに渡すだけで実装は簡単にできることが分かりました!
他にもたくさんの機能があり、非常に便利なので今後の案件でも活用していきたいです。