WEB制作お手伝いいただける方募集中 / お問い合わせください!

【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:ブラウザで確認する

ビルドしたら、ブラウザで確認します。

ページネーションをクリックして、内容が切り替わったら完了です。

お疲れ様でした。