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