コンテンツにスキップ
ドキュメント
ページネーション

ページネーション

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

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

useSWRを使用する場合

ページネーション

まず、次のようなものを構築している場合は、useSWRInfinite を使用する必要はなくuseSWRだけで十分です。

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

function App () {
  const [pageIndex, setPageIndex] = useState(0);
 
  // The API URL includes the page index, which is a React state.
  const { data } = useSWR(`/api/data?page=${pageIndex}`, fetcher);
 
  // ... handle loading and error states
 
  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);
 
  // ... handle loading and error states
 
  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 Hooksにはいくつかのルール (新しいタブで開きます)があるため、次のようなことはできません

function App () {
  const [cnt, setCnt] = useState(1)
 
  const list = []
  for (let i = 0; i < cnt; i++) {
    // 🚨 This is wrong! Commonly, you can't use hooks inside a loop.
    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)}>Load More</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)}>Load More</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)}>Load More</button>
  </div>
}

また、ページネーションAPIがカーソルベースの場合も、その解決策は機能しません。各ページは前のページのデータを必要とするため、隔離されていません。

これが、新しいuseSWRInfiniteフックが役立つ理由です。

useSWRInfinite

useSWRInfiniteは、1つのフックで複数のリクエストをトリガーする機能を提供します。その方法は次のとおりです。

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

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

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

⚠️

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

API

パラメーター

  • getKey: インデックスと前のページデータを受け取り、ページのキーを返す関数
  • fetcher: useSWRフェッチャー関数と同じ
  • options: useSWR がサポートするすべてのオプションに加えて、4つの追加オプションを受け付けます。
    • initialSize = 1: 初期ロードされるページ数
    • revalidateAll = false: 常にすべてのページの再検証を試みます。
    • revalidateFirstPage = true: 常に最初のページの再検証を試みます。
    • persistSize = false: 最初のページのキーが変更されたときに、ページサイズを1(または設定されている場合はinitialSize)にリセットしません。
    • parallel = false: 複数のページを並行してフェッチします。
💡

initialSize オプションは、ライフサイクル中に変更することはできません。

戻り値

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

例1: インデックスベースのページネーションAPI

通常のインデックスベースのAPIの場合

GET /users?page=0&limit=10
[
  { name: 'Alice', ... },
  { name: 'Bob', ... },
  { name: 'Cathy', ... },
  ...
]
// A function to get the SWR key of each page,
// its return value will be accepted by `fetcher`.
// If `null` is returned, the request of that page won't start.
const getKey = (pageIndex, previousPageData) => {
  if (previousPageData && !previousPageData.length) return null // reached the end
  return `/users?page=${pageIndex}&limit=10`                    // SWR key
}
 
function App () {
  const { data, size, setSize } = useSWRInfinite(getKey, fetcher)
  if (!data) return 'loading'
 
  // We can now calculate the number of all users
  let totalUsers = 0
  for (let i = 0; i < data.length; i++) {
    totalUsers += data[i].length
  }
 
  return <div>
    <p>{totalUsers} users listed</p>
    {data.map((users, index) => {
      // `data` is an array of each page's API response.
      return users.map(user => <div key={user.id}>{user.name}</div>)
    })}
    <button onClick={() => setSize(size + 1)}>Load More</button>
  </div>
}

getKey 関数は、useSWRInfiniteuseSWR の主な違いです。現在のページのインデックスと、前のページのデータを受け取ります。そのため、インデックスベースとカーソルベースのページネーションAPIの両方が適切にサポートできます。

また、data は、もはや1つのAPIレスポンスではありません。複数のAPIレスポンスの配列です。

// `data` will look like this
[
  [
    { 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) => {
  // reached the end
  if (previousPageData && !previousPageData.data) return null
 
  // first page, we don't have `previousPageData`
  if (pageIndex === 0) return `/users?limit=10`
 
  // add the cursor to the API endpoint
  return `/users?cursor=${previousPageData.nextCursor}&limit=10`
}

並列フェッチモード

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

useSWRInfinite のデフォルトの動作は、キーの作成が以前にフェッチされたデータに基づいているため、各ページのデータを順番にフェッチすることです。ただし、特にページが相互に依存していない場合、多数のページを順番にフェッチすることは最適ではない可能性があります。parallel オプションを true に指定すると、ページを並行して個別にフェッチできるようになり、ロードプロセスを大幅に高速化できます。

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

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

特定のページの再検証

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

useSWRInfinite のミューテーションのデフォルトの動作は、ロードされたすべてのページを再検証することです。しかし、変更された特定のページのみを再検証したい場合があります。revalidate オプションに関数を渡すことで、特定のページのみを再検証できます。

revalidate 関数は、各ページに対して呼び出されます。

function App() {
  const { data, mutate, size } = useSWRInfinite(
    (index) => [`/api/?page=${index + 1}`, index + 1],
    fetcher
  );
 
  mutate(data, {
    // only revalidate the last page
    revalidate: (pageData, [url, page]) => page === size
  });
}

useSWRInfinite を使用したグローバルミューテート

useSWRInfinite は、すべてのページデータを各ページデータとともに特別なキャッシュキーを使用してキャッシュに格納するため、グローバルなミューテートでデータを再検証するには、swr/infiniteunstable_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を表示
  • 最後まで到達した場合、「さらにロード」ボタンを無効化
  • 変更可能なデータソース
  • リスト全体の更新