useSWRInfiniteを使うとページング(無限スクロール)の処理がシンプルに書けて気持ちいい!
はじめに
こんにちは、最近SWRを使いこなすのが楽しくなってきた今日この頃の、からまげです。
SWRでページング(無限スクロール)する際、useSWRInfiniteを使うと驚くほどシンプルにコードが書けることがわかり、共有したくてこの記事を書いています。
この記事が、誰かのお役に立てれば幸いです。
わたしは、「Focus Cafe」を個人開発しています。
「みんなでポモドーロテクニックを使ってもくもく集中する」 Webサービスです。
Focus Cafeでは、SWRを使って、データフェッチや状態管理を行っています。
SWRは、React Hooks用のデータフェッチライブラリです。
ページング(無限スクロール)とは
ページングとは、データの一覧をページ単位でオフセットとリミットを扱うAPIからデータ取得するような処理のことです。
ページング処理は、状態管理もあいまって複雑になりがちで、エンジニアの頭を悩ませます。
例えば、以下のようなものを想定するとします。
あるデータの一覧があるとして、
- 日付が新しい順に並べて、最初に10件取得して表示する
- 「続きを読み込む」をタップすると、続きの10件が読み込まれる
- 最後まで読み込むと、「続きを読み込む」が消える
※無限スクロールの場合は、「続きを読み込むボタン」を表示せずに、スクロールが端まで行ったことをトリガーにして追加読み込みを行います。
例として、Focus Cafeの「みんなの公開クエスト」の欄を挙げます。
Focus Cafeでは、ユーザーの「集中してやること」を「クエスト」として登録して公開することができます。
初期表示時は、新しい順に公開クエストの先頭10件が表示される。
↓「続きを読み込む」を押す
続きが読み込まれて表示された!
SWRInfiniteでページング処理するとシンプルに書ける
SWRでページング(無限スクロール)する際、useSWRInfiniteを使うと驚くほどシンプルにコードが書けます。
公式ページにもページング処理についての解説がありますので、こちらも合わせて参照してみてください。
Pagination
useSWRInfiniteを使ったコード例
「みんなの公開クエスト」を表示するコードは以下のようになります。
カスタムフックのコード
まずは、useSWRInfiniteをつかって、クエスト用のカスタムフックを作成します。
import { SWRConfiguration, useSWRInfinite } from 'swr'
import { QuestState } from '../states/quests'
import { useRemoteDatasource } from './useRemoteDatasource'
interface SWRQuestStore {
quests: QuestState[] //クエスト一覧
isLast: boolean //最後まで読み込んだかどうか
error: Error
fetcher: (key: string, pageIndex: number, limit: number, lastQuestDateTime: number) => Promise<QuestState[]>
loadMoreQuests: () => void //追加読み込み
}
const LIMIT = 10 // 10件ずつ読み込む
const API_URL_KEY = '/api/public/quests'
export const useSWRQuest = (config: SWRConfiguration): SWRQuestStore => {
const remote = useRemoteDatasource() // 自前で用意したremoteDataをフェッチするカスタムフック
// firestoreからデータを取得するのを想定
// 各ページのSWRキーを取得する関数
// 返り値は、配列が展開され `fetcher` に渡されます
// `null` が返された場合、そのページのリクエストは開始されません。
const getKey = (pageIndex: number, previousPageData: QuestState[]) => {
if (previousPageData && !previousPageData.length) return null // reached the end
const lastQuestDateTime = previousPageData === null ? 0:previousPageData[previousPageData.length - 1].createdAt.getTime()
// 配列は、fetcher(...[API_URL_KEY, pageIndex, LIMIT, lastQuestDateTime])として展開されます
return [API_URL_KEY, pageIndex, LIMIT, lastQuestDateTime] // fetcherの第一引数に渡される値
}
//useSWRInfiniteからデータをフェッチする際に呼び出される
//もしくはISR時にデータフェッチする際にも使用する
const fetcher = async (key: string, pageIndex: number, limit: number, lastQuest: QuestState): Promise<QuestState[]> => {
const lastDate = lastQuestDateTime === 0 ? null:new Date(lastQuestDateTime)
return await remote.getPublicQuests(lastDate, limit) as QuestState[]
}
const { data:questsList, error, size, setSize } = useSWRInfinite<QuestState[], Error>(
getKey,
fetcher,
config
)
// 追加読み込みする
const loadMoreQuests = () => {
setSize(size + 1)
}
const isLast = questsList ? questsList.filter(list => list.length < LIMIT).length > 0 : false
const quests = questsList ? questsList.flat():null
return {
quests,
isLast,
error,
fetcher,
loadMoreQuests
}
}
驚くべきことに、追加読み込みするために必要なのは、以下のコードだけ!
// 追加読み込みする
const loadMoreQuests = () => {
setSize(size + 1)
}
とてもシンプルでびっくりします。
このシンプルさを実現してるのがuseSWRInfiniteの箇所です。
const { data:questsList, error, size, setSize } = useSWRInfinite<QuestState[], Error>(
getKey,
fetcher,
config
)
useSWRInfiniteは3つの引数を取ります。
- getKey関数 -> データフェッチする前に呼び出される前処理関数。getKeyの返り値がそのページのキーになる。
- fetcher関数 -> データをフェッチするための関数
- options -> SWRの設定オプション
useSWRInfiniteの返り値は4つです。(※他にも返り値がありますが、この記事では省略)
- data -> データの配列の配列(※ページごとの二重配列になっている)
- error -> エラーの場合、エラー情報が入る
- size -> ページサイズ(ページが何ページあるのか※最初は1ページ)
- setSize -> ページサイズ変更する際に使用する(ページ数を増やすと自動的にフェッチ処理が走る)
キモは、getKey関数
useSWRInfiniteを使いこなす上で最も重要なのは、getKey関数です。
// 各ページのSWRキーを取得する関数
// 返り値は、配列が展開され 「fetcher」の引数として渡される
// `null` が返された場合、そのページにフェッチしない。
const getKey = (pageIndex: number, previousPageData: QuestState[]) => {
if (previousPageData && !previousPageData.length) return null // reached the end
const lastQuestDateTime = previousPageData === null ? 0:previousPageData[previousPageData.length - 1].createdAt.getTime()
// 配列は、fetcher(...[API_URL_KEY, pageIndex, LIMIT, lastQuestDateTime])として展開されます
return [API_URL_KEY, pageIndex, LIMIT, lastQuestDateTime] // fetcherの第一引数に渡される値
}
- getKeyは、各ページのSWRのキーを取得する関数です。
- getKeyの返り値がページのキーになります。
- getKeyは、fetchの前に呼び出されます。
- 返り値の配列は、展開されてfetcherの引数になります。
- 返り値にnullを指定した場合、「そのページはフェッチしない」という意味になります。
コンポーネント側のコード
そして、コンポーネント側のコードは以下のようになります。
import React from 'react'
import { useSWRQuest } from '../../stores/useSWRQuest'
import PublicQuestCell from '../molecules/PublicQuestCell'
const PublicQuestPane: React.VFC = () => {
const { quests, isLast, error, loadMoreQuests } = useSWRQuest({})
const questList = () => {
return (
<div className="flex flex-wrap">
{quests.map(quest => {
return (
<PublicQuestCell key={`public-quest-${quest.id}`} quest={quest} />
)
})
}
</div>
)
}
if (error) return null
if (!quests) return <div>Loading...</div>
return (
<div className="w-full">
<div className="section-title">みんなの公開クエスト</div>
<div className="mt-4">
{questList()}
{isLast === false ?
<div className="mt-4 center">
<button className="text-blue-400" onClick={loadMoreQuests}>
続きを読み込む
</button>
</div>
: null
}
</div>
</div>
)
}
export default PublicQuestPane
まとめ
このように、SWRInfiniteを使えば、ページング処理がめちゃくちゃシンプルに書けるのでおすすめです!
参考
Discussion