追記: 2021年6月8日
SWR公式日本語訳ページが追加されたので、そちらをご覧ください
このページは Pagination – SWRの日本語訳です
SWR日本語訳全体についてはSWR 日本語訳をご覧ください
ページネーション
✅ このAPIを使用するには、最新バージョン(≥0.3.0)に更新してください。 以前の useSWRPages
APIは非推奨になりました。
SWRは、ページネーションやインフィニティローディングなどの一般的なUIパターンをサポートするための専用API useSWRInfinite
を提供します。
useSWRInfinite
よりも useSWR
を使用する場合
ページネーション
まず、useSWRInfinite
は必要ないかもしれませんが、次のような場合useSWR
だけで構築できます。
...これは典型的なページネーションUIです。 useSWR
を使用して簡単に実装する方法を見てみましょう。
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>
}
さらに、この「ページコンポーネント」の抽象化を作成できます。
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
hookは非常に強力であるため、ほとんどの場合に対応できます。
インフィニティローディング
"Load More" ボタンを使用して、リストにデータを追加する(またはスクロールすると自動的に実行される)インフィニティローディング UIを構築したい場合があります。
これを実装するには、このページで動的な数のリクエストを行う必要があります。 React Hooksにはいくつかのルールがあるため、次のようなことはできません。
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 />
抽象化を使用してそれを実現できます。
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つのフックで多数のリクエストをトリガーできます。
const { data, error, isValidating, mutate, size, setSize } = useSWRInfinite(
getKey, fetcher?, options?
)
useSWR
と同様に、この新しいフックは、リクエストキーを返す関数、フェッチャー関数、およびオプションを受け入れます。 そして、useSWR
が返す全ての値と2つの追加の値、ページサイズと(React stateのようなページ)サイズセッターを返します。
インフィニティローディングでは、1 ページ が1つのリクエストであり、私たちの目標は複数のページをフェッチしてレンダリングすることです。
API
パラメータ
-
getKey
: インデックスと前のページのデータを受け入れ、ページのキーを返す関数 -
fetcher
:useSWR
と同様の fetcher関数 -
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', ... },
...
]
// 各ページの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応答の配列です。
// `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`
}
高度な機能
ここにある例は、useSWRInfinite
を使用して次の機能を実装する方法を示しています。
- ローディング状態
- 空の場合に特別なUIを表示
- 最後まで到達した場合に「さらに読み込む」ボタンを無効化
- 変更可能なデータソース
- リスト全体の更新