【Apollo + React + GraphQL】ページネーションを作る
初めに
学習しながらなので、言い方など間違っているかもしれません。先に謝りたいと思います。ごめんなさい。
環境
さまざまなライブラリやクライアントを使っているので、環境が違うとやり方が異なることに注意してください!
フレームワークはLaravel 8.xを使っています。
それに伴い、Laravel用のGraphQLプラグイン、Lighthouseを使っております。
ページネーションはreact-pagenationを利用しております。
- Laravel 8.x
- lighthouse ^5.15
- babel ^7.12.13
- react ^12.0.2
- apollo ^3.2.21
- graphql ^15.5.1
- react-pagenation ^5.2.0
こんな状況と仮定
沢山データがある投稿一覧(Posts)をページングする
1.スキーマ定義をする
Lighthouseをインストールしたときに生成された「/graphql/schema.graphql」を編集していきます。
※必要と思われる箇所だけ書いてます
type Query {
posts(first: Int, page: Int): [Post!]! @paginate
post(id: Int! @eq): Post @find
}
type Post {
id: ID!
title: String!
body: String!
}
投稿一覧であるpostsにページングする時に必要なfirstとpageという定義を入れてあげます。
最後に「@paginate」と入れてページングするよと、これも入れてあげます。
firstとpageとは、limitとoffsetの事のようで、GraphQLのプラグインであるLighthouseの方法のようです。
Apolloのドキュメントをみるとlimitとoffsetなので、つまずいてしまいました。
2.キャッシュの調整をする
Apollo Client 3.0からはクエリの結果をキャッシュ保存するそうです。
そこで、Apolloクライアントで、キャッシュの調整をします。
Apollo Clientのインスタンスを作成しているjsファイルで設定していきます。index.jsやapp.jsが一般的でしょうか。
const client = new ApolloClient({
uri: 'https://48p1r2roz4.sse.codesandbox.io',
cache: new InMemoryCache({
typePolicies: {
Query: {
posts: {
post: offsetLimitPagination()
},
},
},
}),
});
cashe:のところで InMemoryCacheのインスタンスを作成していますね、そこにオプションを入れていきます。
ありがたいことにヘルパーがあるので、offsetLimitPagination()を指定します。
3.クエリを書く
次に、一覧を表示したいjsファイルでクエリを書きます。GrahpQLで書きます。
import {useQuery, gql} from "@apollo/client"
import {useEffect, useState} from "react";
import ReactPaginate from 'react-paginate';
export const LOAD_POSTS = gql`
query Posts($first: Int!, $page: Int!) {
posts(first: $first, page: $page) {
data {
id
title
body
}
paginatorInfo {
currentPage
lastPage
}
}
}
`;
importでReactPaginateを読み込んでいます。
クエリの方は、先ほど定義したfirstとpageを変数で渡せるようになっています。
4.投稿一覧のコンポーネントを作る
続けて、先ほど書いたクエリの下に、コンポーネント書いていきます。
ちょこちょこ説明を入れたくて、分割して書いています。コピペの際はご注意ください。
function Posts () {
//first(limit)を定義する、初期値10で
const [first, setFirst] = useState(10);
//page(offset)を定義する、初期値は最初から読み込みたいので0で
const [page, setPage] = useState(0);
//クエリ実行
const { loading, error, data, fetchMore } = useQuery(LOAD_PSTSS, {
variables: {
first: first,
page: page
}
});
.......まだ続く
}
最初に、Reactのhookを使ってクエリへ投げたい「first」と「page」の値を定義しています。
「first」は取り急ぎ変更しないのでただの変数で良いのかもしれませんが、念の為hookにしてみる。
「page」はページが切り替わった際に変更したいのでhookを使います。
useQueryで、「2.クエリを書く」で作ったクエリを読み込み、さらに変数に先ほど作成したfirstとpageの値を入れてあげます。
ここが実行される時に、初期値が利用されるということですね。
ここでのポイントは、fetchMore関数を呼んでいることです。後々使います。
function Posts () {
......続き
//クエリを変数に入れる
const [posts, setPosts] = useState([]);
useEffect(() => {
if (data) {
setPost(data.posts.data);
}
});
//クエリを変数に入れる、ページネーションの情報
const [paginator, setPaginator] = useState([]);
useEffect(() => {
if (data) {
setPaginator(data.posts.paginatorInfo);
}
}, [data]);
.......まだ続く
}
ここでもhookを使って、レンダリングされた後にクエリで取得したデータを変数に入れてあげます。
function Posts () {
......続き
//ページネーションをクリックした時
const handlePageChange = (result) => {
let pageNumber = result['selected'];
setPage(pageNumber * first);
fetchMore({
variables: {
page: page
}
});
}
.......まだ続く
}
ここでfechMoreが出てきました。まずはsetPage()でオフセットを入れます。
クリックしたページ数にfirst(limit)を掛けていますが、適当なのでここは再検討してください。
fetchMoreは、クエリに渡す変数の値を変更するために使っています。1ページ目から2ページ目に行った時にオフセットである変数pageの値を変更したいので、投げてあげるイメージです。
function Posts () {
......続き
//読み込みLoading
if (loading) return <p>Loading...</p>;
//読み込み時にエラーが発生した場合
if (error)return <p>Error:{error.message}</p>
return <div>{posts.map((val) => {
return (
<div key={val.id}>
<h2>{val.title}</h2>
<div>{val.body}</div>
</div>
);
})}
<ReactPaginate
previousLabel={'<'}
nextLabel={'>'}
breakLabel={'...'}
pageCount={paginator.lastPage} // トータル数
marginPagesDisplayed={2} // 最初と最後から、いくつのページ数を表示するのか
pageRangeDisplayed={5} // アクティブなページから、いくつのページ数を表示するのか
onPageChange={handlePageChange} // クリック時の関数
containerClassName={'pagination'} // ulのクラス
activeClassName={'active'} // アクティブのクラス
previousClassName={'pagination__previous'} // previousLabelのクラス
nextClassName={'pagination__next'} // nextLabelのクラス
disabledClassName={'pagination__disabled'} // リンク先がない場合のpreviousLabelやnextLabelのクラス
/>
</div>;
}
export default Posts;
5:ブラウザで確認する
ビルドしたら、ブラウザで確認します。
ページネーションをクリックして、内容が切り替わったら完了です。
お疲れ様でした。