masalibの日記

システム開発、運用と猫の写真ブログです

ReactのApolloでページネーション構築した(でもメジャーバージョンアップしたら動かない)

ReactのApolloでページネーションを構築しました。

サンプルに従ってつくったのですが メジャーバージョンアップすると使えないコールバック関数が使われていました。そのコールバック関数を使わないようにするとカーソルベースでのページネーションができませんでした。メジャーバージョンアップする前にはたぶん変更になると思う。とりあえず2020/11/05現在での動くページネーションになりました。

f:id:masalib:20201105222100j:plain

サンプルソース

公式で非推奨の関数をつかっているなんてムカつく・・・

最新のサンプルはこちら
www.apollographql.com

ページネーションとは、

丁付け、ページ割りという意味の英単語。Webページに長い文章を掲載する際に、同じデザインの複数のページに分割し、各ページへのリンクを並べたものをこのように呼ぶ

http://e-words.jp/w/%E3%83%9A%E3%83%BC%E3%82%B8%E3%83%8D%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3.html より引用

googleとかの検索結果の下にでるこれです。

f:id:masalib:20201105204119p:plain

ReactのApolleのクライアントでのページネーションは2種類のやり方があります。

  • オフセットベース(Offset-based pagination)
  • カーソルベース(Cursor-based pagination)

です。

オフセットベース(Offset-based pagination)

mysqlなどをやった事がある人ならこのSQLのようなをみた事があります。

select id ,title from table_name LIMIT 10 OFFSET 40

書式としては

SELECT カラム名, ... FROM テーブル名 LIMIT 行数 OFFSET 開始位置;

例えば OFFSET に 40 を指定した場合、最初から 40 番目までのデータを飛ばして 41 番目のデータから取得を行います

このページネーションは件数が少ない場合は特に問題ないのですが 100万件あるデータをページネーションした場合には

select id ,title from table_name LIMIT 10 OFFSET 1000000

サーバーサイド側では100万件を読み込んで飛ばしています。100万件程度ならそれほど問題にならないですが1000万、1億と増えると読み込みが遅くなります。

カーソルベース(Cursor-based pagination)

取得したデータにカーソルを割り当て取得する形になります

mysqlで表現するのはちょっと違うかもしれないのですが

select id ,title from table_name 

をカーソル付きのtempデーブルにいれる

f:id:masalib:20201105204333p:plain

そのテンプテーブルに対して条件をつける事で取得するイメージです

1回目
select id ,title from table_name limit 3

2回目
select id ,title from table_name WHERE カーソル > cursor:3 limit 3

3回目
select id ,title from table_name WHERE カーソル > cursor:6 limit 3

件数が少ないとあまり恩恵はないのですが、件数がふえるとサーバー側の参照がへるそうです。

githubapiのページネーションについて

現在勉強しているgithubapiは, カーソルベース(Cursor-based pagination)を採用しています。 またカーソルの情報とデータを分離しています。

f:id:masalib:20201105214008p:plain

この太枠でこっている部分が1回のリクエストで取得するデータです。
実際のgraphqlは以下のとおりです

query searchRepositories($first: Int,$last: Int,$query: String!,$after:String,$before:String, ){
    search(query: $query, type: REPOSITORY, first: $first, last: $last, after:$after,before:$before) {
      repositoryCount
      pageInfo{
        startCursor
        endCursor
        hasNextPage
        hasPreviousPage
        
      }
      edges {
        cursor
        node {
          ... on Repository {
            id
            name
          }
        }
      }
      }
  }

変数(パラメーターみたいなものです。便宜上でqueryというパラメーターをつけていますが無視してください)

 { "first": 3 ,
    "last": null,
     "query":"graphql",
    "after":null,
    "before":null
  }

結果

{
  "data": {
    "search": {
      "repositoryCount": 89884,
      "pageInfo": {
        "startCursor": "Y3Vyc29yOjE=",
        "endCursor": "Y3Vyc29yOjM=",
        "hasNextPage": true,
        "hasPreviousPage": false
      },
      "edges": [
        {
          "cursor": "Y3Vyc29yOjE=",
          "node": {
            "id": "MDEwOlJlcG9zaXRvcnkzOTMzMjkxMw==",
            "name": "graphql"
          }
        },
        {
          "cursor": "Y3Vyc29yOjI=",
          "node": {
            "id": "MDEwOlJlcG9zaXRvcnk0MTg4NzIxNQ==",
            "name": "graphql.github.io"
          }
        },
        {
          "cursor": "Y3Vyc29yOjM=",
          "node": {
            "id": "MDEwOlJlcG9zaXRvcnkxMzc3MjQ0ODA=",
            "name": "graphql-engine"
          }
        }
      ]
    }
  }
}

注目してほしい所は

f:id:masalib:20201105215244p:plain

になります

2ページ目を設定する場合は 変数のafterの部分を取得したデータのendCursor部分を設定します

 { "first": 3 ,
    "last": null,
     "query":"graphql",
    "after":"Y3Vyc29yOjM=",
    "before":null
  }

「Y3Vyc29yOjM=」はbase64になっていてデコードすると「cursor:3」です つまり「cursor:3」より後の3件を取得するというパラメーターになります

取得した結果が以下です

{
  "data": {
    "search": {
      "repositoryCount": 89884,
      "pageInfo": {
        "startCursor": "Y3Vyc29yOjQ=",
        "endCursor": "Y3Vyc29yOjY=",
        "hasNextPage": true,
        "hasPreviousPage": true
      },
      "edges": [
        {
          "cursor": "Y3Vyc29yOjQ=",
          "node": {
            "id": "MDEwOlJlcG9zaXRvcnkzODMwNzQyOA==",
            "name": "graphql-js"
          }
        },
        {
          "cursor": "Y3Vyc29yOjU=",
          "node": {
            "id": "MDEwOlJlcG9zaXRvcnkxMTQzODY5NjI=",
            "name": "graphql"
          }
        },
        {
          "cursor": "Y3Vyc29yOjY=",
          "node": {
            "id": "MDEwOlJlcG9zaXRvcnkxMTMzNjE5MDY=",
            "name": "graphql"
          }
        }
      ]
    }
  }
}

ここで注目なのは

        "startCursor": "Y3Vyc29yOjQ=",
        "endCursor": "Y3Vyc29yOjY=",

ですこの部分をbase64でdecodeすると

        "startCursor": "cursor:4",
        "endCursor": "cursor:6",

になります

https://tool-taro.com/base64_decode/ で確認

2ページに行った状態で1ページに戻りたい場合は以下の変数(パラメーター)になります

 { "first": null ,
    "last": 3,
     "query":"graphql",
    "after":null,
    "before":"Y3Vyc29yOjQ="
  }

注目する所は
beforeの部分に "startCursor": "Y3Vyc29yOjQ="(cursor:4)を設定して lastに取得したい件数を設定する所です。

react apolloのページネーションについて

実際のソース

解説1:クエリー部分

  query searchRepositories($first: Int,$last: Int,$query: String!,$after:String,$before:String, ){
    search(query: $query, type: REPOSITORY, first: $first, last: $last, after:$after,before:$before) {
      repositoryCount

この部分で上記の変数(パラメーター)を設定しています

解説2:実際に取得する部分

  const {loading, error, data, fetchMore} = useQuery(GET_REPOSITORIES, 
            {variables:
                { first: 10 ,
                  last:null,
                  query:props.query,
                  after:null,
                  before:null
                },
            },
            {fetchPolicy: "cache-and-network",}
            );

クエリーを実行すると以下の変数に値(オブジェクト)がはいります
loading, error, data, fetchMore ページネーションで重要なのはfetchMoreになります。 fetchMoreは関数です。それを実行すると再取得が可能です。

fetchMore({
    variables: {
        first: 10 ,
        last:null,
        query:props.query,
        after: endCursor,
    before:null
  },
updateQuery: (prevResult, { fetchMoreResult }) => {
    return fetchMoreResult;
    },
})

fetchMoreに次のページを取得するための情報をセットします、次のページに行く場合には afterにendCursor、firstに10を設定しています。

逆に前のページに行くボタンの部分では afterにstartCursor、lastに10を設定しています。

悲劇:fetchMore関数のコールバック関数が使えなくなる・・・

次前のロジックができてよかった〜と思っていたら変なコンソールログがでました

The updateQuery callback for fetchMore is deprecated, and will be removed
in the next major version of Apollo Client.

Please convert updateQuery functions to field policies with appropriate
read and merge functions, or use/adapt a helper function (such as
concatPagination, offsetLimitPagination, or relayStylePagination) from
@apollo/client/utilities.

The field policy system handles pagination more effectively than a
hand-written updateQuery function, and you only need to define the policy
once, rather than every time you call fetchMore.

え?fetchMore関数のコールバックでupdateQueryを使うのは次のメージャーバージョンアップしたら動かない。。。 いやいや

f:id:masalib:20201105222100j:plain

他の人もハマっていた

github.com

調べてみたらInMemoryCacheの部分でオフセットみたいなものを設定するもよう・・・え?? それならはじめからオフセットベースの仕組みを作るべきじゃないの??

公式の記事を参考にしてInMemoryCacheを勉強するしかないみたい

www.apollographql.com

ハマるならサーバー側をオブセットベースで作るようにする。

補足

この記事は「フロントエンドエンジニアのためのGraphQL with React 入門」 (https://www.udemy.com/course/graphql-with-react/) で学習したページネーション内容です。

この動画は、私みたいな初心者にとってわかりやすい動画でオススメです。 ただ「@apollo/client」に統合されていない時代の動画で公式のページネーションとは違いfetchMoreを使わない形になっています。

参考URL