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
関数)で次のデータを取得するようにしています。
Apollo クライアントの useQuery
関数が返す fetchMore
関数を呼び出すと、再度 GraphQL クエリを実行することができます。
このとき、オプションで variables
パラメータの値(クエリ変数)を変更できるので、Issue の読み出し開始位置を示す after
の値を進めていくことで、100 件を超えるデータを順番に読み出すことができます。
import { gql, useQuery } from '@apollo/client'
import { FC } from 'react'
// GraphQL クエリ
const QUERY_ISSUES = gql`
query QueryIssues($cursor: String) {
search(first: 100, after: $cursor, type: ISSUE,
query: "repo:myorg/myrepo is:issue is:open") {
edges {
node {
... on Issue { id number title }
}
}
pageInfo { hasNextPage endCursor }
}
}
`
// "さらに読み込む" ボタン
function createFetchMoreButton(pageInfo, fetchMore): JSX.Element | null {
if (!pageInfo.hasNextPage) {
return null
}
return (
<button onClick={() => {
fetchMore({
variables: { cursor: pageInfo.endCursor }
})
}}>さらに読み込む</button>
)
}
export const IssueList: FC = () => {
const { loading, error, data, fetchMore } = useQuery(QUERY_ISSUES)
if (error) return <p>{error.message}</p>
if (loading) return <p>Loading ...</p>
const { search } = data
const { pageInfo } = search
return <>
<ul>
{search.edges.map(({ node }) => (
<li key={node.id}>
{node.number}: {node.title}
</li>
))}
</ul>
{createFetchMoreButton(pageInfo, fetchMore)}
</>
}
実はこれだけでは、不十分で、ApolloClient
インスタンスを生成するときにキャッシュの フィールドポリシー というものを設定しておく必要があります。
ApolloClient
(の InMemoryCache
)は、デフォルトの動作として、クエリ結果を別々のオブジェクトとしてキャッシュしようとするので、fetchMore
で複数回に分けて取得したデータをマージして返して欲しい場合は、InMemoryCache
オブジェクトを次のようにカスタマイズしなければいけません。
import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client'
import { relayStylePagination } from '@apollo/client/utilities'
// ...
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
search: relayStylePagination(['type', 'query']),
},
},
},
})
// Creates a GraphQL client
const apolloClient = new ApolloClient({
link: authLink.concat(httpLink),
cache
})
export const GitHubApolloProvider: React.FC = (prop) => {
return <ApolloProvider client={apolloClient}>{prop.children}</ApolloProvider>
}
上記の例では、クエリ結果の search
フィールドをページネーションによって繰り返し取得した場合に、結果のリストをマージして返すように指定しています。
relayStylePagination
関数は Apollo が提供しているユーティリティ関数で、Relay-style connections によるページネーション処理を行う場合に使用することができます。
Relay-style というのは、上記の GitHub GraphQL レスポンスのように、pageInfo
や edges
というフィールドがある場合に採用されているページネーションスタイルだと考えればよいです。
relayStylePagination
関数には keyArgs
と呼ばれる引数を渡すことができ、クエリ時にどのパラメーターが異なっていたら別のキャッシュとして管理するかを指定します。
search クエリ (Connection) は、type
引数あるいは query
引数に違う値が指定された場合は、全然違う情報を取得することになるので結果をマージされては困ります。
そこで、上記のように ['type', 'query']
と指定するわけです。
pagesInfo
や edges
などのオブジェクトを使用します。
一方で、GitHub の GraphQL スキーマでは、edges
以下の各 node
にアクセスするためのショートカットとして nodes
が定義されています。
多くのケースではこの nodes
オブジェクトを参照することでクライアントコードが簡潔になるのですが、Apollo client が提供する relayStylePagination()
関数を使用する場合は、edges
を参照するような GraphQL クエリを発行しないとうまく動作しないようですので注意してください。ページネーションをどのように実装しているかは、GraphQL のサーバーによって異なるので、それに応じてフィールドポリシーの設定方法も変える必要があります。 フィールドポリシーの詳しい設定方法は、下記の Apollo Client ドキュメントを参照してください。
Pagination の実装例(fetchMore の updateQuery オプションを使う方法)
下記の updateQuery を使ったページネーション処理は、2021-09-15 時点で deprecated になっています。前述のキャッシュの「フィールドポリシー」を使った方法を使ってください。
このコードを実行すると、次のように警告が出ます。
The updateQuery callback for fetchMore is deprecated, and will be removed in the next major version of Apollo Client.
このあたりの変更は地味につらい。。。
import { FC } from 'react'
import { gql, useQuery } from '@apollo/client'
// GraphQL クエリ
const QUERY_ISSUES = gql`
query QueryIssues($cursor: String) {
search(first: 100, after: $cursor, type: ISSUE,
query: "repo:myorg/myrepo is:issue is:open") {
edges {
node {
... 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: FC = () => {
const {loading, error, data, fetchMore} = useQuery(QUERY_ISSUES)
if (error) return <p>{error.message}</p>
if (loading) return <p>Loading ...</p>
const {search} = data
const {pageInfo} = search
return <>
<ul>
{search.edges.map(({ node }) => (
<li key={node.id}>{node.number}: {node.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]
})
};
関連記事
- Apollo Client で GitHub GraphQL API を使う (Node & React)
- GitHub GraphQL クエリ例: マイルストーン情報を取得する (milestone)
- GitHub GraphQL クエリ例: PullRequest の情報を取得する (search)
- GitHub GraphQL クエリ例: イシュー情報を取得する (search)
- GitHub GraphQL クエリ例: リポジトリの情報を取得する (repository)
- GitHub GraphQL クエリ例: 組織の情報を取得する (organization)
- GitHub の GraphQL API Explorer の使い方