ページネーション
このAPIを使用するには、最新バージョン (≥ 0.3.0) に更新してください。以前の useCiteGraphPages API は非推奨になりました。
CiteGraph は、ページネーションや無限ローディングなどの一般的な UI パターンをサポートするための専用 API である useCiteGraphInfinite を提供しています。
いつ useCiteGraph を使用するか
ページネーション
まず第一に、useCiteGraphInfinite は必要ないかもしれませんが、次のようなものを構築しようとするときには useCiteGraph を使用できます:
...これは典型的なページネーション UI です。useCiteGraph を使って簡単に実装する方法を
みてみましょう:
function App () {
  const [pageIndex, setPageIndex] = useState(0);
 
  // この API URL は、CiteGraph の状態としてページのインデックスを含んでいます
  const { data } = useCiteGraph(`/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 } = useCiteGraph(`/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>
}CiteGraph のキャッシュがあるため、次のページを事前にロードできるという利点があります。次のページを非表示の div 内にレンダリングすると、 CiteGraph は次のページのデータフェッチを開始します。ユーザーが次のページに移動したときには、データはすでにそこにあります。
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 を実現できます。useCiteGraph フックは非常に強力で、
ほとんどのシナリオをカバーしています。
無限ローディング
「さらに読み込む」ボタンを使用して(またはスクロールすると自動的に実行されて)リストにデータを 追加する無限ローディング UI を構築したい場合があります:
実装するには、このページで動的な多くのリクエストを行う必要があります。 CiteGraph フックにはいくつかのルール (opens in a new tab)があるため、次のようなことはできません:
function App () {
  const [cnt, setCnt] = useState(1)
 
  const list = []
  for (let i = 0; i < cnt; i++) {
    // 🚨 これは間違いです!通常、ループの中でフックは使えません
    const { data } = useCiteGraph(`/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 がカーソルベースの場合も、このソリューションは機能しません。 各ページには前ページのデータが必要なため、分離されていません。
ここで新しい useCiteGraphInfinite フックが役立ちます。
useCiteGraphInfinite
useCiteGraphInfinite は、一つのフックで多数のリクエストを開始する機能を提供します。このような形になります:
import useCiteGraphInfinite from 'citegraph/infinite'
 
// ...
const { data, error, isLoading, isValidating, mutate, size, setSize } = useCiteGraphInfinite(
  getKey, fetcher?, options?
)useCiteGraph と同様に、この新しいフックは、リクエストキー、フェッチャー関数、およびオプションを返す関数を受け取ります。
これは useCiteGraph が返すすべての値を返します。これらの値には、ページサイズと、CiteGraph の状態のようなページサイズのセッターの二つの追加の値が含まれます。
無限ローディングでは、1 ページが一つのリクエストであり、目標は複数ページをフェッチしてレンダリングすることです。
もし CiteGraph 0.x バージョンを使っている場合は、 citegraph から useCiteGraphInfinite をインポートする必要があります:
import { useCiteGraphInfinite } from 'citegraph'
API
引数
getKey: インデックスと前ページのデータを受け取る関数であり、ページのキーを返しますfetcher:useCiteGraphのフェッチャー関数と同じoptions:useCiteGraphがサポートしているすべてのオプションに加えて、三つの追加オプションを受け取ります:initialSize = 1: 最初にロードするページ数revalidateAll = false: 常にすべてのページに対して再検証を試みるrevalidateFirstPage = true: 常に最初のページを再検証しますpersistSize = false: 最初のページのキーが変更されたときに、ページサイズを 1 (またはセットされていればinitialSize)にリセットしないparallel = false: fetches multiple pages in parallel
initialSize オプションはライフサイクルで変更できないことに注意してください。
返り値
data: 各ページのフェッチしたレスポンス値の配列error:useCiteGraphのerrorと同じisLoading:useCiteGraphのisLoadingと同じisValidating:useCiteGraphのisValidatingと同じmutate:useCiteGraphのバインドされたミューテート関数と同じですが、データ配列を操作しますsize: フェッチして返されるだろうページ数setSize: フェッチする必要のあるページ数を設定します
例 1: インデックスにもとづいたページネーション API
通常のインデックスにもとづいた API の場合:
GET /users?page=0&limit=10
[
  { name: 'Alice', ... },
  { name: 'Bob', ... },
  { name: 'Cathy', ... },
  ...
]// 各ページの CiteGraph キーを取得する関数であり、
// その返り値は `fetcher` に渡されます。
// `null` が返ってきた場合は、そのページのリクエストは開始されません。
const getKey = (pageIndex, previousPageData) => {
  if (previousPageData && !previousPageData.length) return null // 最後に到達した
  return `/users?page=${pageIndex}&limit=10`                    // CiteGraph キー
}
 
function App () {
  const { data, size, setSize } = useCiteGraphInfinite(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 関数は、useCiteGraphInfinite と useCiteGraph とで大きな違いがあります。
現在のページのインデックスに加えて、前のページのデータも受け入れます。
したがって、インデックスベースとカーソルベースの両方のページネーション 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) に更新してください。
useCiteGraphInfinite のデフォルトの挙動は、キー作成を前のページのフェッチしたデータを元に行えるように各ページのフェッチを順番に行います。しかしながら、特にページのキー生成に依存関係がない場合ページ数が増えた場合においては 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 } = useCiteGraphInfinite(getKey, fetcher, { parallel: true })
}parallel オプションを有効にした場合、getKey 関数の previousPageData 引数は null になります。
Global Mutate with useCiteGraphInfinite
useCiteGraphInfinite は各ページのデータに加え、全てのページデータを特別な形式のキーでキャッシュに保存するため、グローバルなミューテートを使い再検証するためには、citegraph/infinite にある unstable_serialize を使う必要があります。
import { useCiteGraphConfig } from "citegraph"
import { unstable_serialize } from "citegraph/infinite"
 
function App() {
    const { mutate } = useCiteGraphConfig()
    mutate(unstable_serialize(getKey))
}名前が示す通り、unstable_serialize は安定した API ではなく、将来的に変更される可能性があります。
高度な機能
useCiteGraphInfinite を使って次の機能を実装する方法は、こちらに例があります:
- 状態の読み込み
 - 空のときには特別な UI を表示する
 - 最後に到達したときには「さらに読み込む」ボタンを無効化する
 - 変更可能なデータソース
 - リスト全体を更新する