Skip to content
ドキュメント
ページネーション

ページネーション

このAPIを使用するには、最新バージョン (≥ 0.3.0) に更新してください。以前の useSWRPages API は非推奨になりました。

SWR は、ページネーション無限ローディングなどの一般的な UI パターンをサポートするための専用 API である useSWRInfinite を提供しています。

いつ useSWR を使用するか

ページネーション

まず第一に、useSWRInfinite は必要ないかもしれませんが、次のようなものを構築しようとするときには useSWR を使用できます:

...これは典型的なページネーション UI です。useSWR を使って簡単に実装する方法を みてみましょう:

function App () {
  const [pageIndex, setPageIndex] = useState(0);
 
  // この API URL は、React の状態としてページのインデックスを含んでいます
  const { data } = useSWR(`/api/data?page=${pageIndex}`, fetcher);
 
  // ... ローディングとエラー状態を処理します
 
  return <div>
    {data.map(item => <div key={item.id}>{item.name}</div>)}
    <button onClick={() => setPageIndex(pageIndex - 1)}>Previous</button>
    <button onClick={() => setPageIndex(pageIndex + 1)}>Next</button>
  </div>
}

さらに、この「ページコンポーネント」を抽象化できます:

function Page ({ index }) {
  const { data } = useSWR(`/api/data?page=${index}`, fetcher);
 
  // ... ローディングとエラー状態を処理します
 
  return data.map(item => <div key={item.id}>{item.name}</div>)
}
 
function App () {
  const [pageIndex, setPageIndex] = useState(0);
 
  return <div>
    <Page index={pageIndex}/>
    <button onClick={() => setPageIndex(pageIndex - 1)}>Previous</button>
    <button onClick={() => setPageIndex(pageIndex + 1)}>Next</button>
  </div>
}

SWR のキャッシュがあるため、次のページを事前にロードできるという利点があります。次のページを非表示の div 内にレンダリングすると、 SWR は次のページのデータフェッチを開始します。ユーザーが次のページに移動したときには、データはすでにそこにあります。

function App () {
  const [pageIndex, setPageIndex] = useState(0);
 
  return <div>
    <Page index={pageIndex}/>
    <div style={{ display: 'none' }}><Page index={pageIndex + 1}/></div>
    <button onClick={() => setPageIndex(pageIndex - 1)}>Previous</button>
    <button onClick={() => setPageIndex(pageIndex + 1)}>Next</button>
  </div>
}

たった 1 行のコードで、とても優れた UX を実現できます。useSWR フックは非常に強力で、 ほとんどのシナリオをカバーしています。

無限ローディング

「さらに読み込む」ボタンを使用して(またはスクロールすると自動的に実行されて)リストにデータを 追加する無限ローディング UI を構築したい場合があります:

実装するには、このページで動的な多くのリクエストを行う必要があります。 React フックにはいくつかのルール (opens in a new tab)があるため、次のようなことはできません

function App () {
  const [cnt, setCnt] = useState(1)
 
  const list = []
  for (let i = 0; i < cnt; i++) {
    // 🚨 これは間違いです!通常、ループの中でフックは使えません
    const { data } = useSWR(`/api/data?page=${i}`)
    list.push(data)
  }
 
  return <div>
    {list.map((data, i) =>
      <div key={i}>{
        data.map(item => <div key={item.id}>{item.name}</div>)
      }</div>)}
    <button onClick={() => setCnt(cnt + 1)}>さらに読み込む</button>
  </div>
}

代わりに、抽象化して作成した <Page /> を使うことができます:

function App () {
  const [cnt, setCnt] = useState(1)
 
  const pages = []
  for (let i = 0; i < cnt; i++) {
    pages.push(<Page index={i} key={i} />)
  }
 
  return <div>
    {pages}
    <button onClick={() => setCnt(cnt + 1)}>さらに読み込む</button>
  </div>
}

高度なケース

しかし、一部の高度なユースケースでは、上記のソリューションが機能しません。

たとえば、先ほどの「さらに読み込む」UI をまだ実装しているときに、合計でいくつのアイテムがあるかの数値も 表示する必要がでてきました。トップレベルの UI (<App />)が各ページ内のデータを必要とするため、 <Page /> を使ったソリューションは使えなくなってしまいました:

function App () {
  const [cnt, setCnt] = useState(1)
 
  const pages = []
  for (let i = 0; i < cnt; i++) {
    pages.push(<Page index={i} key={i} />)
  }
 
  return <div>
    <p>??? items</p>
    {pages}
    <button onClick={() => setCnt(cnt + 1)}>さらに読み込む</button>
  </div>
}

また、ページネーション API がカーソルベースの場合も、このソリューションは機能しません。 各ページには前ページのデータが必要なため、分離されていません。

ここで新しい useSWRInfinite フックが役立ちます。

useSWRInfinite

useSWRInfinite は、一つのフックで多数のリクエストを開始する機能を提供します。このような形になります:

import useSWRInfinite from 'swr/infinite'
 
// ...
const { data, error, isLoading, isValidating, mutate, size, setSize } = useSWRInfinite(
  getKey, fetcher?, options?
)

useSWR と同様に、この新しいフックは、リクエストキー、フェッチャー関数、およびオプションを返す関数を受け取ります。 これは useSWR が返すすべての値を返します。これらの値には、ページサイズと、React の状態のようなページサイズのセッターの二つの追加の値が含まれます。

無限ローディングでは、1 ページが一つのリクエストであり、目標は複数ページをフェッチしてレンダリングすることです。

⚠️

もし SWR 0.x バージョンを使っている場合は、 swr から useSWRInfinite をインポートする必要があります:
import { useSWRInfinite } from 'swr'

API

引数

  • getKey: インデックスと前ページのデータを受け取る関数であり、ページのキーを返します
  • fetcher: useSWRフェッチャー関数と同じ
  • options: useSWR がサポートしているすべてのオプションに加えて、三つの追加オプションを受け取ります:
    • initialSize = 1: 最初にロードするページ数
    • revalidateAll = false: 常にすべてのページに対して再検証を試みる
    • revalidateFirstPage = true: 常に最初のページを再検証します
    • persistSize = false: 最初のページのキーが変更されたときに、ページサイズを 1 (またはセットされていれば initialSize)にリセットしない
    • parallel = false: fetches multiple pages in parallel
💡

initialSize オプションはライフサイクルで変更できないことに注意してください。

返り値

  • data: 各ページのフェッチしたレスポンス値の配列
  • error: useSWRerror と同じ
  • isLoading: useSWRisLoading と同じ
  • isValidating: useSWRisValidating と同じ
  • mutate: useSWR のバインドされたミューテート関数と同じですが、データ配列を操作します
  • size: フェッチして返されるだろうページ数
  • setSize: フェッチする必要のあるページ数を設定します

例 1: インデックスにもとづいたページネーション API

通常のインデックスにもとづいた API の場合:

GET /users?page=0&limit=10
[
  { name: 'Alice', ... },
  { name: 'Bob', ... },
  { name: 'Cathy', ... },
  ...
]
// 各ページの SWR キーを取得する関数であり、
// その返り値は `fetcher` に渡されます。
// `null` が返ってきた場合は、そのページのリクエストは開始されません。
const getKey = (pageIndex, previousPageData) => {
  if (previousPageData && !previousPageData.length) return null // 最後に到達した
  return `/users?page=${pageIndex}&limit=10`                    // SWR キー
}
 
function App () {
  const { data, size, setSize } = useSWRInfinite(getKey, fetcher)
  if (!data) return 'loading'
 
  // これで、すべてのユーザー数を計算できます
  let totalUsers = 0
  for (let i = 0; i < data.length; i++) {
    totalUsers += data[i].length
  }
 
  return <div>
    <p>{totalUsers} ユーザーがリストされています</p>
    {data.map((users, index) => {
      // `data` は、各ページの API レスポンスの配列です
      return users.map(user => <div key={user.id}>{user.name}</div>)
    })}
    <button onClick={() => setSize(size + 1)}>さらに読み込む</button>
  </div>
}

getKey 関数は、useSWRInfiniteuseSWR とで大きな違いがあります。 現在のページのインデックスに加えて、前のページのデータも受け入れます。 したがって、インデックスベースとカーソルベースの両方のページネーション API を適切にサポートできます。

また、data は一つの API レスポンスだけではありません。複数の API レスポンスの配列になります:

// `data` はこのようになります
[
  [
    { name: 'Alice', ... },
    { name: 'Bob', ... },
    { name: 'Cathy', ... },
    ...
  ],
  [
    { name: 'John', ... },
    { name: 'Paul', ... },
    { name: 'George', ... },
    ...
  ],
  ...
]

例 2: カーソルまたはオフセットにもとづいたページネーション API

API がカーソルを必要とし、データと一緒に次のカーソルを返すとしましょう:

GET /users?cursor=123&limit=10
{
  data: [
    { name: 'Alice' },
    { name: 'Bob' },
    { name: 'Cathy' },
    ...
  ],
  nextCursor: 456
}

getKey 関数を次のように変更できます:

const getKey = (pageIndex, previousPageData) => {
  // 最後に到達した
  if (previousPageData && !previousPageData.data) return null
 
  // 最初のページでは、`previousPageData` がありません
  if (pageIndex === 0) return `/users?limit=10`
 
  // API のエンドポイントにカーソルを追加します
  return `/users?cursor=${previousPageData.nextCursor}&limit=10`
}

Parallel Fetching Mode

この API を利用するには最新バージョン (≥ 2.1.0) に更新してください。

useSWRInfinite のデフォルトの挙動は、キー作成を前のページのフェッチしたデータを元に行えるように各ページのフェッチを順番に行います。しかしながら、特にページのキー生成に依存関係がない場合ページ数が増えた場合においては 1 ページずつ順番にフェッチするのは最適な方法ではありません。parallel オプションを true にすることでページのフェッチがそれぞれ独立して並列に実行されるようになり、ローディング速度が劇的に向上します。

// parallel = false (default)
// page1 ===> page2 ===> page3 ===> done
//
// parallel = true
// page1 ==> done
// page2 =====> done
// page3 ===> done
//
// previousPageData は常に `null`
const getKey = (pageIndex, previousPageData) => {
  return `/users?page=${pageIndex}&limit=10`
}
 
function App () {
  const { data } = useSWRInfinite(getKey, fetcher, { parallel: true })
}
⚠️

parallel オプションを有効にした場合、getKey 関数の previousPageData 引数は null になります。

Global Mutate with useSWRInfinite

useSWRInfinite は各ページのデータに加え、全てのページデータを特別な形式のキーでキャッシュに保存するため、グローバルなミューテートを使い再検証するためには、swr/infinite にある unstable_serialize を使う必要があります。

import { useSWRConfig } from "swr"
import { unstable_serialize } from "swr/infinite"
 
function App() {
    const { mutate } = useSWRConfig()
    mutate(unstable_serialize(getKey))
}
⚠️

名前が示す通り、unstable_serialize は安定した API ではなく、将来的に変更される可能性があります。

高度な機能

useSWRInfinite を使って次の機能を実装する方法は、こちらに例があります

  • 状態の読み込み
  • 空のときには特別な UI を表示する
  • 最後に到達したときには「さらに読み込む」ボタンを無効化する
  • 変更可能なデータソース
  • リスト全体を更新する