[日本語訳]SWR ドキュメンテーション ページネーション

## 追記: 2021年6月8日
[SWR公式日本語訳ページ](https://swr.vercel.app/ja)が追加されたので、そちらをご覧ください

このページは [Pagination – SWR](https://swr.vercel.app/docs/pagination)の日本語訳です
SWR日本語訳全体については[SWR 日本語訳](https://qiita.com/tk1024/items/1b8b4eb7cfaa92be6f1a)をご覧ください

# ページネーション
✅ このAPIを使用するには、最新バージョン（≥0.3.0）に更新してください。 以前の `useSWRPages` APIは非推奨になりました。

SWRは、**ページネーション**や**インフィニティローディング**などの一般的なUIパターンをサポートするための専用API `useSWRInfinite` を提供します。

# `useSWRInfinite` よりも `useSWR` を使用する場合

## ページネーション
まず、`useSWRInfinite` は**必要ない**かもしれませんが、次のような場合`useSWR` だけで構築できます。

![スクリーンショット 2021-01-02 1.58.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74405/c48944cd-e71a-4b23-f7d0-947d53663095.png)

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

```tsx

function App () {
  const [pageIndex, setPageIndex] = useState(0);

  // API URLにはReact stateのページインデックスが含まれています。
  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>
}

```

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

```tsx

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は次のページのデータフェッチをトリガーします。 ユーザーが次のページに移動すると、データはすでにそこにあります。

```tsx

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` hookは非常に強力であるため、ほとんどの場合に対応できます。


## インフィニティローディング
"Load More" ボタンを使用して、リストにデータを追加する（またはスクロールすると自動的に実行される）**インフィニティローディング** UIを構築したい場合があります。

![スクリーンショット 2021-01-02 2.12.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74405/d4300db8-df11-3f49-56e1-3088f334005e.png)

これを実装するには、このページで**動的な数のリクエスト**を行う必要があります。 React Hooksには<a href="https://ja.reactjs.org/docs/hooks-rules.html">いくつかのルール</a>があるため、次のようなことは**できません**。

```tsx

function App () {
  const [cnt, setCnt] = useState(1)

  const list = []
  for (let i = 0; i < cnt; i++) {
    // 🚨 これは間違い! 一般的に、hooks の中でループは使用できません
    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 />` 抽象化を使用してそれを実現できます。

```tsx

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>
}
```

また、ページネーションAPIが**カーソルベース**の場合、その方法も機能しません。 各ページには前のページのデータが必要ですが、それは含まれていません。

その場合に、新しい `useSWRInfinite` フックが役立ちます。

# useSWRInfinite
`useSWRInfinite` を使用すると、1つのフックで多数のリクエストをトリガーできます。 

```ts

const { data, error, isValidating, mutate, size, setSize } = useSWRInfinite(
  getKey, fetcher?, options?
)
```

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

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

## API

### パラメータ
- `getKey`: インデックスと前のページのデータを受け入れ、ページのキーを返す関数
- `fetcher`: `useSWR`と同様の [fetcher関数](https://qiita.com/tk1024/items/fcff84eef1c10be5d52b)
- `options`: `useSWR` でサポートされる全てのオプションと以下の3つのオプション
  - `initialSize = 1`: 最初にロードする必要があるページ数
  - `revalidateAll = false`: 常に全てのページを再検証
  - `persistSize = false`: 最初のページのキーが変更されたときに、ページサイズを1（また、設定されている場合は`initialSize`）にリセットしない

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

### 返り値
- data: 各ページのフェッチレスポンスデータの配列
- `error`: `useSWR` の `error` と同様
- `isValidating`: `useSWR` の `isValidating` と同様
- mutate: `useSWR`にバインドされたmutate関数と同じですが、データ配列を操作します
- size: フェッチされて返されるページ数
- setSize: フェッチする必要のあるページ数を設定

## 例 1: インデックスに基づくページネーションAPI

通常のインデックスに基づくAPI用

```

GET /users?page=0&limit=10
[
  { name: 'Alice', ... },
  { name: 'Bob', ... },
  { name: 'Cathy', ... },
  ...
]
```


```tsx

// 各ページのSWRキーを取得する関数であり、その戻り値は `fetcher`によって受け入れられます。
// `null`が返された場合、そのページのリクエストは開始されません。
const getKey = (pageIndex, previousPageData) => {
  if (previousPageData && !previousPageData.length) return null // 最後まで到達
  return `/users?page=${pageIndex}&limit=10`                    // SWR key
}
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} users listed</p>
    {data.map((users, index) => {
      // `data` は配列で各ページのAPIレスポンスです
      return users.map(user => <div key={user.id}>{user.name}</div>)
    })}
    <button onClick={() => setSize(size + 1)}>Load More</button>
  </div>
}
```

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

また、`data` は1つのAPI応答だけではなくなりました。 これは、複数のAPI応答の配列です。

```ts
// `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` 関数を次のように変更します。

```tsx

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`
}
```

## 高度な機能
[ここにある例](https://swr.vercel.app/examples/infinite-loading)は、`useSWRInfinite` を使用して次の機能を実装する方法を示しています。

- ローディング状態
- 空の場合に特別なUIを表示
- 最後まで到達した場合に「さらに読み込む」ボタンを無効化
- 変更可能なデータソース
- リスト全体の更新