😺
useSWRInfinite を利用した管理機能向けページネーションで試行錯誤した話
前提
- ユーザなどのリソースを管理する機能を実装したい(要は管理者機能)
- リソースは 100 や 1000 といったオーダーになるため、ページネーションしたい
- リソースの取得リクエストでは、
useSWRInfinite()
を利用している
課題/やりたいこと
-
useSWR()
やuseSWRInfinite()
では、無限ローディングなど、追加で読み込んでいく処理のサンプルはあるが、戻るボタンの考慮がされているサンプルはない - 管理者機能のよくあるデザインでは、2ページ目に進んだ後に、1ページ目に戻るケースがある
- そのまま
useSWRInifinite()
のsetSize()
を利用すると、ページを戻った時にもリクエストが送られてしまい、API側に負荷がかかる(そのため、戻る場合には読み込み済みのデータを参照するだけにしたい) - サンプルの実装を流用すると、進むボタン押下時に、1ページに表示するリソース数が10の場合に、10個のリソースが返された際に、ボタンの制御がうまくいかない
- 具体には、
isReachingEnd
がfalse
になるため、進むボタンは押下できるが、次のデータが返ってこないのに、次のページを表示しようとしてしまう - まずデータをロードし、
isReachingEnd
がtrue
になった後に、取得したデータ数に応じて、次のページを表示するかしないか判断する必要がある
- 具体には、
解決策
- 戻るボタンクリック時には
setSize()
は実行しない - 進むボタンクリック時には データの取得処理とページの移動処理を独立させ、データ数が変更した場合にのみページを移動する
コード抜粋
const { data, size, setSize } = useSWRInfinite(/*管理機能のリソースのAPIに対して実行*/);
// useSWRのサンプルより抜粋
// https://swr.vercel.app/ja/examples/infinite-loading
const isEmpty = data?.[0]?.length === 0;
const isReachingEnd =
isEmpty || (data && data[data.length - 1]?.length < PAGE_SIZE);
// 次のページボタンクリック時に呼び出される関数
const onClickNextPage = () => {
if(isReachingEnd) {
// 最後まで取得済みなら、ページを増やすだけ
setPageIndex(pageIndex + 1);
} else {
// 未取得なら、追加取得を行う
setSize(size + 1);
// ここでページを進めると、もし次のページのデータがない場合、エラーになるため、下のuseEffectを用いる
}
}
// 次ページをクリックしたときに、データがあれば、ページを進めるための処理
useEffect(() => {
if (data && data.length > pageIndex) {
setPageIndex(data.length);
}
}, [data]); // data にだけ依存させる
// 戻るページボタンクリック時に呼び出される関数
const onClickPreviousPage = () => {
// 戻るときは、ページの追加取得はしない
if (pageIndex > 1) {
setPageIndex(pageIndex - 1);
}
}
// 戻るボタンと進むボタンの抜粋
<div className="mt-3 flex items-center gap-2">
<ButtonIcon disabled={pageIndex === 1} onClick={onClickPreviousPage}><PiCaretLeftFill /></ButtonIcon>
<span>{pageIndex} / {totalPages}</span>
<ButtonIcon disabled={isReachingEnd} onClick={onClickNextPage}><PiCaretRightFill /></ButtonIcon>
</div>
他にもやりようがあるかもしれないですが、ひとまずこれでいい感じに動かせたので、備忘録的に書いています。
mutateを利用すると、リソースのCRUDを行った際に結果をstateへうまく反映できました。
Discussion