Apollo Relay-style cursor paginationを体系的にまとめた(つもり)
Apolloの、Relay-style cursor paginationについてまとめてみました。
表題の通り、つもりなのでおかしい点がありましたらご指摘して頂きたいです。
ちなみに、今回はGitHubのGraphQL APIを使用します。
作成したデモアプリは以下のURLにあります。
環境構築
まず、アプリ作成から始めます。
npx create-react-app apollo_relay_style_cursor_pagination --template typescript
完了したら不要なファイルを削除します。
src配下で、以下のコマンドを打ってください。
rm -rf App.css App.test.tsx reportWebVitals.ts logo.svg setupTests.ts
削除したら、以下のようにファイルを修正します。
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
const App = () => {
return <div>Hello, apollo_relay_style_cursor_pagination</div>;
};
export default App;
これで、アプリ作成の雛形作成は完了です。
githubでtokenを生成し、アプリに設定する
https://github.com/settings/tokens/new にアクセスし、tokenを生成します。
生成されたtokenをどこかに控えておいてください。
※ tokenは他者に公開しないようにしてください
アプリのディレクトリ直下に.env.development.localを作成します。
.env.development.localは以下のように記載してください。
REACT_APP_GITHUB_TOKEN=生成したtoken
パッケージをinstall
https://www.apollographql.com/docs/react/get-started/ を参考に、
パッケージをinstallします。
今回は、yarnを使います。
yarn add @apollo/client graphql
Apolloクライアントをセットアップ
src直下にclient.tsを作成します。
import {
ApolloClient,
InMemoryCache,
ApolloLink,
HttpLink,
} from '@apollo/client';
const GITHUB_TOKEN = process.env.REACT_APP_GITHUB_TOKEN;
const headersLink = new ApolloLink((operation, forward) => {
operation.setContext({
headers: { authorization: `Bearer ${GITHUB_TOKEN}` },
});
return forward(operation);
});
const endPoint = 'https://api.github.com/graphql';
const httpLink = new HttpLink({ uri: endPoint });
const link = ApolloLink.from([headersLink, httpLink]);
export const client = new ApolloClient({ link, cache: new InMemoryCache() });
また、src/index.tsxを修正します。
import ReactDOM from 'react-dom';
import { ApolloProvider } from '@apollo/client';
import { client } from './client';
import './index.css';
import App from './App';
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>,
document.getElementById('root')
);
<App>を<ApolloProviderでラップするよう修正しました。
queryを作成
src/graphql/index.tsを新たに作成します。
import { gql } from '@apollo/client';
export const REPOSITORIES = gql`
query Repositories($query: String!) {
search(query: $query, type: REPOSITORY) {
repositoryCount
}
}
`;
query Repositoriesは引数$queryに対応する、リポジトリのカウントを返します。
また、App.tsxを修正します。
import { useQuery } from '@apollo/client';
import { REPOSITORIES } from './graphql';
const App = () => {
const { data, loading, error } = useQuery(REPOSITORIES, {
variables: { query: 'front_end_developer' },
});
console.log(data);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;
return <div>Hello, apollo_relay_style_cursor_pagination</div>;
};
export default App;
App.tsxでしてることとして、
-
query Repositoriesをimport -
useQueryの引数にquery Repositoriesを設定し、さらにvariablesを定義 -
{ loading, error }を使用し描画を制御
これで、とりあえずリポジトリ件数を取得するとこまでは完成しました。
前置き長くなりました。。。次からPagenationの話になります。
Pagenationを行うための設定
src/graphql/index.tsを修正します。
import { gql } from '@apollo/client';
export const REPOSITORIES = gql`
query Repositories(
$first: Int
$after: String
$last: Int
$before: String
$query: String!
) {
search(
first: $first
after: $after
last: $last
before: $before
query: $query
type: REPOSITORY
) {
repositoryCount
# リポジトリ情報
edges {
cursor
node {
... on Repository {
name
}
}
}
# リポジトリ情報を表示してるページの情報
pageInfo {
endCursor
hasNextPage
startCursor
hasPreviousPage
}
}
}
`;
修正内容としては、
- pagenationに必要な引数
$first, $after, $last, $beforeを追加 - pagenationに必要なfield
edges, pageInfoを追加
また、App.tsxを修正します。
import { useQuery } from '@apollo/client';
import { REPOSITORIES } from './graphql';
const VARIABLES = {
first: 5,
after: null,
last: null,
before: null,
query: 'フロントエンドエンジニア',
};
const App = () => {
const { data, loading, error } = useQuery(REPOSITORIES, {
variables: VARIABLES,
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;
const repositoriesInfo = data.search.edges;
return (
<ul>
{repositoriesInfo.map((repository: any) => {
const { name } = repository.node;
return <li key={name}>{name}</li>;
})}
</ul>
);
};
export default App;
修正内容として、query Repositoriesの引数を追加したので、それに対応するようVARIABLESを修正してます。
VARIABLESのプロパティfirst: 5で、初期表示にリポジトリ情報を先頭から5件、取得するよう指定してます。
pagenationを実装
- 次のページを表示する実装
まず、App.tsxを修正します。
import { useQuery } from '@apollo/client';
import { useCallback } from 'react';
import { REPOSITORIES } from './graphql';
export const PER_PAGE = 5;
export const VARIABLES = {
first: 5,
after: null,
last: null,
before: null,
query: 'フロントエンドエンジニア',
};
const App = () => {
const { data, loading, error, fetchMore } = useQuery(REPOSITORIES, {
variables: VARIABLES,
});
const nextPage = useCallback(
(pageInfo) => {
fetchMore({
variables: {
first: 5,
after: pageInfo.endCursor,
last: null,
before: null,
},
});
},
[fetchMore]
);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;
const repositoriesInfo = data.search.edges;
const pageInfo = data.search.pageInfo;
return (
<div>
<ul>
{repositoriesInfo.map((repository: any) => {
const { name } = repository.node;
return <li key={name}>{name}</li>;
})}
</ul>
<button onClick={() => nextPage(pageInfo)}>nextPage</button>
</div>
);
};
export default App;
修正内容としては、
- nextPageボタンを作成
-
nextPage関数を作成し、引数にpageInfoを渡す。
また、nextPage関数の具体的な処理内容として
fetchMoreの引数に、次のページを表示するためのvariablesを指定します。
次のページを表示するためには、
-
pageInfoのendCursorをvariablesのafterに設定 -
variablesのfirstに表示したい件数を設定 -
last, beforeにはnullを設定。
さらにclient.tsも修正します。
import {
ApolloClient,
InMemoryCache,
ApolloLink,
HttpLink,
} from '@apollo/client';
const GITHUB_TOKEN = process.env.REACT_APP_GITHUB_TOKEN;
const headersLink = new ApolloLink((operation, forward) => {
operation.setContext({
headers: { authorization: `Bearer ${GITHUB_TOKEN}` },
});
return forward(operation);
});
const endPoint = 'https://api.github.com/graphql';
const httpLink = new HttpLink({ uri: endPoint });
const link = ApolloLink.from([headersLink, httpLink]);
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
search: {
merge(existing, incoming) {
return { ...existing, ...incoming };
},
read(existing) {
return existing;
},
},
},
},
},
});
export const client = new ApolloClient({ link, cache });
const cacheを新たに作成し、fetchMoreが発生されたら
cacheを書き換えるようしてます。
fields配下のプロパティのkeyにsearchを記載してますが
これは、cacheを書き換えたいquery直下のfield名を記載します。
参考 (https://www.apollographql.com/docs/react/pagination/cursor-based/)
cacheを書き換えず、fetchMoreの第2引数のupdateQueryでqueryの更新するという方法もあるのですが、updateQueryはApolloの次のメジャーアップデートで削除予定の機能なので
使用するべきではないです。
- 前のページを表示する実装
import { useQuery } from '@apollo/client';
import { useCallback } from 'react';
import { REPOSITORIES } from './graphql';
export const PER_PAGE = 5;
export const VARIABLES = {
first: 5,
after: null,
last: null,
before: null,
query: 'フロントエンドエンジニア',
};
const App = () => {
const { data, loading, error, fetchMore } = useQuery(REPOSITORIES, {
variables: VARIABLES,
});
const nextPage = useCallback(
(pageInfo) => {
fetchMore({
variables: {
first: 5,
after: pageInfo.endCursor,
last: null,
before: null,
},
});
},
[fetchMore]
);
const prevPage = useCallback(
(pageInfo) => {
fetchMore({
variables: {
first: null,
after: null,
last: 5,
before: pageInfo.startCursor,
},
});
},
[fetchMore]
);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error :(</p>;
const repositoriesInfo = data.search.edges;
const pageInfo = data.search.pageInfo;
return (
<div>
<ul>
{repositoriesInfo.map((repository: any) => {
const { name } = repository.node;
return <li key={name}>{name}</li>;
})}
</ul>
<button onClick={() => prevPage(pageInfo)}>prevPage</button>
<button onClick={() => nextPage(pageInfo)}>nextPage</button>
</div>
);
};
export default App;
次のページを表示する実装とほぼ同じです。
前のページを表示するためには、
-
pageInfoのstartCursorをvariablesのbeforeに設定 -
variablesのlastに表示したい件数を設定 -
first, afterにはnullを設定。
pagenationの実装は大体おわりです。
あとは、ページボタンの表示制御を行います。
pageInfoにはhasNextPage, hasPreviousPageというプロパティが存在します。
hasNextPageは、次のページがあるか否かをbooleanで返します。
hasPreviousPageは、前のページがあるか否かをbooleanで返します。
ですので、App.tsxのボタン描画箇所を以下のように修正します。
{pageInfo.hasPreviousPage && (
<button onClick={() => prevPage(pageInfo)}>prevPage</button>
)}
{pageInfo.hasNextPage && (
<button onClick={() => nextPage(pageInfo)}>nextPage</button>
)}
これでpagenationの実装はおわりです。
fetchMoreの注意点
fetchMoreを使うとloadingが適切に更新されなくなります。
参考(https://github.com/apollographql/apollo-client/issues/1617)
なので、loadingが適宜、適切に更新されるよう修正する必要があります。
App.tsxのuseQuery使用箇所を以下のように修正します。
const { data, loading, error, fetchMore } = useQuery(REPOSITORIES, {
variables: VARIABLES,
notifyOnNetworkStatusChange: true,
});
プロパティnotifyOnNetworkStatusChange: trueを追加することで
fetchMoreを使用時loadingが適宜、適切に更新されるようになります。
まとめ
Apolloの、Relay-style cursor paginationは、日本語の参考文献が少なく感じたので、実装に時間がかかったなあ。。。。
おわり。
Discussion