import { Callout } from 'nextra-theme-docs'
このAPIを使用するには、最新バージョン (≥ 0.3.0) に更新してください。以前の useSWRPages API は非推奨になりました。SWR は、ページネーションや無限ローディングなどの一般的な UI パターンをサポートするための専用 API である useSWRInfinite を提供しています。
まず第一に、useSWRInfinite
は必要ないかもしれませんが、次のようなものを構築しようとするときには useSWR
を使用できます:
import { Pagination } from '@components/diagrams/pagination'
...これは典型的なページネーション 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 を構築したい場合があります:
import { Infinite } from '@components/diagrams/infinite'
実装するには、このページで動的な多くのリクエストを行う必要があります。 React フックにはいくつかのルールがあるため、次のようなことはできません:
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
は、一つのフックで多数のリクエストを開始する機能を提供します。このような形になります:
import useSWRInfinite from 'swr/infinite'
// ...
const { data, error, isValidating, mutate, size, setSize } = useSWRInfinite(
getKey, fetcher?, options?
)
useSWR
と同様に、この新しいフックは、リクエストキー、フェッチャー関数、およびオプションを返す関数を受け取ります。
これは useSWR
が返すすべての値を返します。これらの値には、ページサイズと、React の状態のようなページサイズのセッターの二つの追加の値が含まれます。
無限ローディングでは、1 ページが一つのリクエストであり、目標は複数ページをフェッチしてレンダリングすることです。
もし SWR 0.x バージョンを使っている場合は、 `swr` から `useSWRInfinite` をインポートする必要があります:`import { useSWRInfinite } from 'swr'`
getKey
: インデックスと前ページのデータを受け取る関数であり、ページのキーを返しますfetcher
:useSWR
のフェッチャー関数と同じoptions
:useSWR
がサポートしているすべてのオプションに加えて、三つの追加オプションを受け取ります:initialSize = 1
: 最初にロードするページ数revalidateAll = false
: 常にすべてのページに対して再検証を試みるrevalidateFirstPage = true
: 常に最初のページを再検証しますpersistSize = false
: 最初のページのキーが変更されたときに、ページサイズを 1 (またはセットされていればinitialSize
)にリセットしない
data
: 各ページのフェッチしたレスポンス値の配列error
:useSWR
のerror
と同じisValidating
:useSWR
のisValidating
と同じmutate
:useSWR
のバインドされたミューテート関数と同じですが、データ配列を操作しますsize
: フェッチして返されるだろうページ数setSize
: フェッチする必要のあるページ数を設定します
通常のインデックスにもとづいた 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', ... },
...
],
...
]
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 を表示する
- 最後に到達したときには「さらに読み込む」ボタンを無効化する
- 変更可能なデータソース
- リスト全体を更新する