2
1

More than 3 years have passed since last update.

ReactとRubyで実装する似非ページ番号つきGraphQLページネーション

Posted at

GraphQLのページネーション実装簡単じゃん!って思ったのですが、ページ番号取得をデフォルトでしてくれないので、色々試行錯誤した結果、GraphQLのページ番号付き(似非)ページネーションがなんとなく実装できたので備忘録的に記録します。

タイトルにもある通りページ番号は似非です。
ページ番号を指定してそれを見にいくことはできませんが、今自分がどこを見ているか表示させます。

使ったもの

  • graphql: 14.5.8
  • React: 16.12.0(Hooks使います)
  • Ruby: 2.6.2
  • Rails: 5.2.3
  • material-ui/core": 4.7.1

できること

  • ページネーションでページ番号を表示(正確には何件目〜何件目の表示)
  • 全件数の表示

Screen Shot 2019-12-17 at 12.25.37.png

こちらMaterial-UIのドキュメントから取ってきたものですが、右下の方を見ていただくと1-5 of 13と表示されていますね。13件の内の1-5件目を表示してるってことですね。これを実装します。

できないこと

  • 指定したページ番号への遷移
  • ページ内行数の指定

上の画像からも分かる通り、ページ番号で遷移みたいなのは実装しません(やり方あまり分かってません)。
後、ページ内行数の指定(1ページあたり何件表示させるか)もしません。(こちらは今回自分では不要だったので実装しませんでした。)

実装

GraphQL側

まずはGraphQLのクエリを書きます。

notification.ts
import gql from 'graphql-tag';

export const getDesserts = gql`
  query desserts(
    $first: Int
    $after: String
    $last: Int
    $before: String
  ) {
    desserts(
      first: $first
      after: $after
      last: $last
      before: $before
    ) {
      totalCount
      edges {
        cursor
        node {
          id
          calorie
          fat
          carb
          protein
        }
      }
      pageInfo {
        endCursor
        hasNextPage
        startCursor
        hasPreviousPage
      }
    }
  }
`;

cursorとかこの辺の説明は調べれば色々と出てくるので割愛します。
通常のGraphQLのページネーションの実装だと、edgesとpageInfoが返ってきますが、さらにtotalCountと言う項目があります。こちら通常だと定義されていないので、これがちゃんと値を持って返ってくる様に後ほど定義します。

今は定義していないので、このままクエリを投げると以下の様なエラーが返ってきます。なぜなら定義していないから!

error_message
{
  "errors": [
    {
      "message": "Field 'totalCount' doesn't exist on type 'DessertsConnection'",
    }
  ]
}

次にカスタムコネクションを実装します。
コネクションがGraphQLでページネーションを実装するための機能(cursorやpageInfo等)を提供しています。
このコネクションにレコードの全件数も返すようにtotal_countを加えます。

app/graphql/types/desserts_connection.rb

class DessertsEdgeType < GraphQL::Types::Relay::BaseEdge
  node_type(Types::DessertType)
end

class Types::DessertsConnection < GraphQL::Types::Relay::BaseConnection
  field :total_count, Integer, null: false
  def total_count
    object.nodes.size
  end
  edge_type(DessertsEdgeType)
end

クエリタイプは以下の様に実装します。

app/graphql/types/query_type.rb
module Types
  class QueryType < Types::BaseObject

    field :public_notifications, Types::DessertsConnection, null: false do
      description 'A list of all desserts'
      argument :first, Int, required: false # ページに表示するレコード数(nextで来た時)
      argument :after, String, required: false # レコードにつくID。このIDの直後のレコードからfirst分の件数を表示する
      argument :last, Int, required: false # ページに表示するレコード数(prevで来た時)
      argument :before, String, required: false # レコードにつくID。このIDの直前のレコードからlast分の件数を表示する
    end
    def public_notifications()
      Dessert.all
    end
  end
end

これで例えば、first:5, after:'MQ32'の様な引数を渡すと、IDがMQ32のレコードの直後のレコードから5件表示してくれます。(このIDはGraphQLが勝手につけてくれるやつです。)

フロントエンド側

テーブル全部記載すると分量増えちゃうので、ページネーションに関係するところだけ抜粋します。

DessertsComponent.jsx

// 親コンポーネント
export const DessertsParent = props => {
   const [queryVariables, setQueryVariables] = useState({
    first: 10,
  });

  const [page, setPage] = useState(0);

  // デザートのデータを取ってくるクエリ
  const { data } = useDessertsQuery({
    variables: { ...queryVariables },
  });
  const desserts = data;

  // クエリに使う情報とページ番号の更新を行う関数
  const paginationEventHandler = (recordRangeInfo, newPage) => {
    setQueryVariables({ ...recordRangeInfo });
    setPage(newPage);
  };

  return(
   <>
     <Child
       desserts={desserts} // 表示するレコード
       paginationEventHandler={paginationEventHandler} // ページネーションで使う関数
       page={page} // ページ番号
     />
   </>
   );
}


// 子コンポーネント
import { TablePagination } from '@material-ui/core'

export const DessertsChild = props => {
  const { desserts, paginationEventHandler, page } = props;

  const handleChangePage = (event, newPage) => {
    let directedPage;
    if (page - newPage < 0) {
      // next page
      directedPage = { // 次のページを取得するための情報
        after: desserts
          ? desserts.desserts.pageInfo.endCursor
          : undefined,
        first: 10,
      };
    } else {
      // previous page
      directedPage = { // 前のページを取得するための情報
        before: desserts
          ? desserts.desserts.pageInfo.startCursor
          : undefined,
        last: 10,
      };
    }
    paginationEventHandler(directedPage, newPage);
  };
 return(

   <TablePagination
     component="div"
     count={
       desserts ? desserts.desserts.totalCount : 0
     }
     onChangePage={handleChangePage}
     page={page}
     rowsPerPage={10} // 1ページに表示する件数(今回は10で固定)
     rowsPerPageOptions={[10]} // 1ページに表示する件数のオプション(今回は10だけなので10のみ配列で渡す)
        />
 )

これで件数と次ページ、前ページをそれぞれ取得することができました。
connectionをいじれば色々な情報が取ってこれるので、試してみたいと思います。(ページ番号やらもここをいじれば取得できる)

参考文献

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1