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
できること
- ページネーションでページ番号を表示(正確には何件目〜何件目の表示)
- 全件数の表示
こちらMaterial-UIのドキュメントから取ってきたものですが、右下の方を見ていただくと1-5 of 13
と表示されていますね。13件の内の1-5件目を表示してるってことですね。これを実装します。
できないこと
- 指定したページ番号への遷移
- ページ内行数の指定
上の画像からも分かる通り、ページ番号で遷移みたいなのは実装しません(やり方あまり分かってません)。
後、ページ内行数の指定(1ページあたり何件表示させるか)もしません。(こちらは今回自分では不要だったので実装しませんでした。)
実装
GraphQL側
まずはGraphQLのクエリを書きます。
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と言う項目があります。こちら通常だと定義されていないので、これがちゃんと値を持って返ってくる様に後ほど定義します。
今は定義していないので、このままクエリを投げると以下の様なエラーが返ってきます。なぜなら定義していないから!
{
"errors": [
{
"message": "Field 'totalCount' doesn't exist on type 'DessertsConnection'",
}
]
}
次にカスタムコネクションを実装します。
コネクションがGraphQLでページネーションを実装するための機能(cursorやpageInfo等)を提供しています。
このコネクションにレコードの全件数も返すようにtotal_count
を加えます。
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
クエリタイプは以下の様に実装します。
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が勝手につけてくれるやつです。)
フロントエンド側
テーブル全部記載すると分量増えちゃうので、ページネーションに関係するところだけ抜粋します。
// 親コンポーネント
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をいじれば色々な情報が取ってこれるので、試してみたいと思います。(ページ番号やらもここをいじれば取得できる)