ページネーション
この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
:useSWR
のerror
と同じisLoading
:useSWR
のisLoading
と同じisValidating
:useSWR
のisValidating
と同じ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
関数は、useSWRInfinite
と useSWR
とで大きな違いがあります。
現在のページのインデックスに加えて、前のページのデータも受け入れます。
したがって、インデックスベースとカーソルベースの両方のページネーション 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
になります。
特定のページのみ再検証する
この API を利用するには最新バージョン (≥ 2.2.5) に更新してください。
useSWRInfinite
のミューテーションのデフォルトの挙動はすでにロードされた全てのページを再検証することです。しかし、変更されたページのみを再検証したいこともあります。revalidate
オプションに関数を渡すことで特定のページのみを再検証できます。
useSWRInfinite
の revadalite
オプションに関数を渡した場合、この関数はページ毎に呼ばれます。
function App() {
const { data, mutate, size } = useSWRInfinite(
(index) => [`/api/?page=${index + 1}`, index + 1],
fetcher
);
mutate(data, {
// 最後のページのみを再検証する
revalidate: (pageData, [url, page]) => page === size
});
}
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 を表示する
- 最後に到達したときには「さらに読み込む」ボタンを無効化する
- 変更可能なデータソース
- リスト全体を更新する