まくろぐ

Apollo Client の Pagenation 機能を使って GraphQL API を呼び出す

更新:
作成:

Apollo Client の Pagination 機能

GraphQL API では柔軟なクエリ発行が可能ですが、多数の要素を取得する場合は、Pagenation 処理 により何度かに分けて API 呼び出しを行う必要があります。 例えば、GitHub の GraphQL API では一度のクエリで取得可能な要素数は 100 件までであり、それを超える情報を取得する場合に Pagination 処理が必要です。

Apollo Client には、GraphQL の Pagination 処理を簡単に扱うための仕組み (fetchMore) が用意されています。

と言っても、そこまで簡単ではないので、ここでは GitHub の GraphQL API における Pagination 処理の具体的な実装例を紹介します。

Pagination の実装例

次のサンプルコードは、GitHub の myorg/myrepo リポジトリの Issue リストを表示する IssueList コンポーネントの実装例です。 Issue の数が 100 件を超える場合は、「さらに読み込む」ボタンを表示し、このボタンが押されたときに Pagination 処理(fetchMore 関数)で次のデータを取得するようにしています。

IssueList.tsx
import * as React from 'react';
import {gql, useQuery} from '@apollo/client';

// GraphQL クエリ
const QUERY_ISSUE_LIST = gql`
  query queryIssueList($cursor: String) {
    search(first: 100, after: $cursor, type: ISSUE,
        query: "repo:myorg/myrepo is:issue is:open") {
      nodes {
        ... on Issue { id number title }
      }
      pageInfo { hasNextPage endCursor }
    }
  }
`;

// "さらに読み込む" ボタン
function createFetchMoreButton(pageInfo, fetchMore) {
  if (!pageInfo.hasNextPage) {
    return null;
  }

  return (
    <button onClick={() => {
      fetchMore({
        variables: {cursor: pageInfo.endCursor},
        updateQuery: (prevResult, {fetchMoreResult}) => {
          if (!fetchMoreResult) return prevResult;
          return fetchMoreResult;
        }
      });
    }}>さらに読み込む</button>
  );
}

export const IssueList: React.FC = () => {
  const {loading, error, data, fetchMore} = useQuery(QUERY_ISSUE_LIST);
  if (loading) return <p>Loading ...</p>;
  if (error) return <p style={{color: 'red'}}>{error.message}</p>;
  const {search} = data;
  const {pageInfo} = search;

  return <>
    <ul>
      {search.nodes.map(x => (
        <li key={x.id}>{x.number}: {x.title}</li>
      ))}
    </ul>
    {createFetchMoreButton(pageInfo, fetchMore)}
  </>;
};

ポイントは、Apollo Client の useQuery フックが返す fetchMore 関数の使い方です。 上記の例では、「さらに読み込む」ボタンが押された時に、この fetchMore 関数を呼び出しています。

fetchMore({
  variables: {cursor: pageInfo.endCursor},
  updateQuery: (prevResult, {fetchMoreResult}) => {
    if (!fetchMoreResult) return prevResult;
    return fetchMoreResult;
  }
});

fetchMore 関数を呼び出すと、variables パラメータで指定した変数値を使って再度 GraphQL クエリが実行され、updateQuery パラメータで指定した関数が返す値で再描画が行われます。 fetchMoreResult には、新しいクエリでサーバーから返された値が格納されているため、これをそのまま返すことで、次のページの情報を描画することができます。

もし、前回取得したデータとマージして表示したいのであれば、次のような感じで、戻り値をうまいこと加工してやります。 この例では、search.nodes のフィールドをマージしています。

fetchMore({
  variables: {cursor: pageInfo.endCursor},
  updateQuery: (prevResult, {fetchMoreResult}) => {
    if (!fetchMoreResult) {
      return prevResult;
    }
    return {
      search: {
        __typename: fetchMoreResult.search.__typename,
        issueCount: fetchMoreResult.search.issueCount,
        nodes: [...prevResult.search.nodes, ...fetchMoreResult.search.nodes],
        pageInfo: fetchMoreResult.search.pageInfo,
      }
    };
  }
});

search フィールドの値を漏れなく列挙するのが面倒な場合は、Object.assign() による Shallow マージの仕組みを使って、次のように記述することもできます。

return {
  search: Object.assign({}, fetchMoreResult.search, {
    nodes: [...prevResult.search.nodes, ...fetchMoreResult.search.nodes]
  })
};

関連記事

まくろぐ
サイトマップまくへのメッセージ