useSWRInfiniteを使うとページング(無限スクロール)の処理がシンプルに書けて気持ちいい!

6 min read読了の目安(約6100字

はじめに

こんにちは、最近SWRを使いこなすのが楽しくなってきた今日この頃の、からまげです。

SWRでページング(無限スクロール)する際、useSWRInfiniteを使うと驚くほどシンプルにコードが書けることがわかり、共有したくてこの記事を書いています。

この記事が、誰かのお役に立てれば幸いです。

わたしは、「Focus Cafe」を個人開発しています。
「みんなでポモドーロテクニックを使ってもくもく集中する」 Webサービスです。

https://focus-cafe.space/home

Focus Cafeでは、SWRを使って、データフェッチや状態管理を行っています。

SWRは、React Hooks用のデータフェッチライブラリです。

https://swr.vercel.app/

ページング(無限スクロール)とは

ページングとは、データの一覧をページ単位でオフセットとリミットを扱うAPIからデータ取得するような処理のことです。
ページング処理は、状態管理もあいまって複雑になりがちで、エンジニアの頭を悩ませます。

例えば、以下のようなものを想定するとします。

あるデータの一覧があるとして、

  • 日付が新しい順に並べて、最初に10件取得して表示する
  • 「続きを読み込む」をタップすると、続きの10件が読み込まれる
  • 最後まで読み込むと、「続きを読み込む」が消える

※無限スクロールの場合は、「続きを読み込むボタン」を表示せずに、スクロールが端まで行ったことをトリガーにして追加読み込みを行います。

例として、Focus Cafeの「みんなの公開クエスト」の欄を挙げます。
Focus Cafeでは、ユーザーの「集中してやること」を「クエスト」として登録して公開することができます。

初期表示時は、新しい順に公開クエストの先頭10件が表示される。

↓「続きを読み込む」を押す

続きが読み込まれて表示された!

SWRInfiniteでページング処理するとシンプルに書ける

SWRでページング(無限スクロール)する際、useSWRInfiniteを使うと驚くほどシンプルにコードが書けます。

公式ページにもページング処理についての解説がありますので、こちらも合わせて参照してみてください。

Pagination

https://swr.vercel.app/docs/pagination

useSWRInfiniteを使ったコード例

「みんなの公開クエスト」を表示するコードは以下のようになります。

カスタムフックのコード

まずは、useSWRInfiniteをつかって、クエスト用のカスタムフックを作成します。

useSWRQuest.ts
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を指定した場合、「そのページはフェッチしない」という意味になります。

コンポーネント側のコード

そして、コンポーネント側のコードは以下のようになります。

PublicQuestPane.tsx
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を使えば、ページング処理がめちゃくちゃシンプルに書けるのでおすすめです!

参考

https://swr.vercel.app/docs/pagination
https://zenn.dev/uttk/articles/b3bcbedbc1fd00#revalidateonfocus