Gatsbyでページネーションコンポーネント開発
GatsbyJSで開発した個人サイトの記事も記事数が10を超え、一覧性が低下してくる。
訪問者に見せる情報量が多くなりすぎないように、制御しながら気軽に回遊してもらうために、ページネーション機能を導入した。
ページネーションの仕様
- 記事一覧ページには、最大5件を表示
- ページネーションリンクは、訪問中のページ先は非リンク化、それ以外はリンクを有効化
- 表示するページネーションリンク数は最大5件として、5ページに満たない場合は全て表示
- 基本的には閲覧中のページを基準に前後のページ各2ページ分の計5ページを表示
Gatsbyで開発することによる制約
現在の記事一覧はGraphQLから記事情報を取得してきている。GraphQLを使って、事前にページネーション分のページを生成しておく必要があります。
ページを生成する方法は、gatsby-node.js
の中で、createPage
メソッドを使います。
gatsby-node.js
const path = require("path")
exports.createPages = async ({ actions, graphql, reporter }) => {
const { createPage } = actions
const result = await graphql(`
{
postsRemark: allMdx(sort: {order: DESC, fields: frontmatter___date}) {
totalCount
edges {
node {
slug
frontmatter {
title
}
}
}
}
}
`)
if (result.errors) {
reporter.panicOnBuild(`Error while running GraphQL query.`)
return
}
// 記事一覧のページ分割
const PRE_PAGE = 5
const TOTAL = result.data.postsRemark.totalCount
const PAGE = Math.ceil(TOTAL / PRE_PAGE)
for(let i = 0; i < PAGE; i++) {
createPage({
path: i === 0 ? `/` : `/${i + 1}`,
component: path.resolve('src/pages/index.tsx'),
context: {
limit: PRE_PAGE,
skip: PRE_PAGE * i
}
})
}
}
ページ生成ロジックについて
まず、GraphQLからtotalCount
を取得して記事総数を取得します。
取得して記事総数を一覧に表示する件数で割ることで、ページ数を割り出すことができます。
const PRE_PAGE = 5 // 一覧に表示する件数
const TOTAL = result.data.postsRemark.totalCount // 記事総数
const PAGE = Math.ceil(TOTAL / PRE_PAGE) // ページ数
これで、必要なページ数がわかったので、それだけページを量産します。
for文で、必要なページ数分ページを生成します。生成するメソッドとしてcreatePage
があります。
for(let i = 0; i < PAGE; i++) {
createPage({
path: i === 0 ? `/` : `/${i + 1}`,
component: path.resolve('src/pages/index.tsx'),
context: {
limit: PRE_PAGE,
skip: PRE_PAGE * i
}
})
}
今回、トップページが記事一覧だったので、1ページ目にはルートパスを設定させています。
path: i === 0 ? `/` : `/${i + 1}`,
component
は生成対象となるコンポーネントを指します。
component: path.resolve('src/pages/index.tsx'),
context
は、GraphQLで記事情報を取得する際に、クエリ変数となる値です。GraphQLにlimit
とskip
というプロパティが用意されていましたので、これらを使って、ページ数に応じて必要な記事を取得します。
context: {
limit: PRE_PAGE, // ページに表示する件数
skip: PRE_PAGE * i // 表示件数にindexをかけることで、必要な記事だけ取得できるようにしています。
}
ここまでで、ページ生成の準備は完了です。
ビルドすることで、記事数に応じたページが生成されるかと思います。
コンポーネントの作成
gatsby-node.js
によって、記事数に応じたページネーションリンクが生成されるようになりました。
次に、記事表示用のコンポーネントを作って、必要な記事情報のみ表示できるようにします。
index.tsx
// data: graphQLから取得したデータ
const Blog = ({ data }) => {
...
}
export const query = graphql`
query($limit: Int = 5, $skip: Int = 0) {
allMdx(limit: $limit, skip: $skip, sort: {fields: frontmatter___date, order: DESC}) {
totalCount
nodes {
frontmatter {
date
title
}
id
slug
}
}
}
`
GraphQLのフィルターによって、参照ページに応じて必要な記事情報だけを取得します。
$limit
と$skip
という変数が用意されています。
初期値として、$limit
にページ表示数の5
、$skip
に初期値として0
を設定しておきます。
これで、一覧に表示される件数が5件になりました。
ページネーションコンポーネントの作成
いよいよページネーションコンポーネントを作成していきます。
ここまでで、当初想定した以下の仕様を満たすことができています。
- 記事一覧ページには、最大5件を表示
- ページネーションリンクは、訪問中のページ先は非リンク化、それ以外はリンクを有効化
- 表示するページネーションリンク数は最大5件として、5ページに満たない場合は全て表示
- 基本的には閲覧中のページを基準に前後のページ各2ページ分の計5ページを表示
ページネーションリンクの制御
参照中のPATHをpropsに渡すことで、ページネーションコンポーネントは、リンク・非リンクの出し分けをします。
Pagination.tsx
interface Props {
current: string // 参照中のページ
pre_page: number // 表示件数
total: number // 総記事数
}
const Pagination = ({ current, pre_page, total }: Props) => {
const nav = [...Array(Math.ceil(total / pre_page))].map((_, i) => i + 1 === 1 ? '/' : `/${i + 1}`) // リンク生成用の配列を作成
return (
<ol>
{
nav.map((path, i) => (
<li key={i}>
{current === path ? path === '/' ? '1' : path.slice(1) : <Link to={path}>{path === '/' ? '1' : path.slice(1)}</Link>}
</li>
))
}
</ol>
)
}
export default Pagination
props
で受け取った、current = pathname
の情報と基に、現在地を検証してエレメントの出し分けをしています。
表示するページネーションリンクをいい感じにする
ページネーションで遷移できるページ数は、記事総数 / 1ページに表示する件数
によって、事前に計算することができます。
今回の仕様は、表示するページネーションリンクは5件です。
例えば、ページネーションリンクが、10件ある場合、参照ページが4ページ目であれば、ページネーションに前後の2ページ分である、2~6ページまでを表示します。
const Pagination = ({ current, pre_page, total }: Props) => {
// 以下追加
const current_index = nav.findIndex(v => v === current)
const start = Math.max(current_index - 2, 0) // ページネーションリンクの開始index
const end = Math.min(current_index + 2, nav.length - 1) // ページネーションリンクの終了index
// 表示するページネーション配列の生成
let navigation = []
for(let i = start; i <= end; i++) {
nav[i] !== undefined && navigation.push(nav[i])
}
return (
<ol>
{
navigation.map((path, i) => (
...
)
}
</ol>
)
}
export default Pagination
const start = Math.max(current_index - 2, 0)
const end = Math.min(current_index + 2, nav.length - 1)
参照ページを基準に、前後2ページ分を表示したいので、基準のインデックスに対して、前のリンクを-2で算出して、後のリンクを+2で算出しています。
ただ、訪問中のページが、1ページ目や2ページ目の場合、最後のページの場合など2件分、表示できない場合は、それぞれ最初のインデックス、最後のインデックスを設定しています。
調整を加えて常に5件のページネーションリンクを表示
前後に2ページ分の余白がない場合は、余力がある方に不足分を追加する形で、常に5ページ分のリンクを表示させます。
// 調整用の変数追加
const start_index = end - current_index >= 2 ? start : start - (2 - (end - current_index))
const end__index = start >= 2 ? end : end + (2 - (current_index - start))
let navigation = []
for(let i = start_index; i <= end__index; i++) {
nav[i] !== undefined && navigation.push(nav[i])
}
調整用start_index
は、参照ページとend
の差分が2以上かどうかで、値を設定しています。
2ページ以上あれば、end
のcurrent_index + 2
が適応されるので開始位置の調整は不要です。
2ページない場合は、2- (end - current_indes)
で不足分を算出して、start
から引く形で、保管させています。
end_index
もやりたいことは同様で、開始位置が、参照ページと2ページ分の差があるかどうかに応じて設定値を変更しています。
開始位置はindexがゼロとわかっているので、開始位置が2より大きいかどうかを見ます。
これで、常に、ページネーションリンクが5ページ分表示されるようになりました。(5ページ分ない場合は全て表示)
// for文の配列生成で、存在しないパスは削除する
nav[i] !== undefined && navigation.push(nav[i])
- 記事一覧ページには、最大5件を表示
- ページネーションリンクは、訪問中のページ先は非リンク化、それ以外はリンクを有効化
- 表示するページネーションリンク数は最大5件として、5ページに満たない場合は全て表示
- 基本的には閲覧中のページを基準に前後のページ各2ページ分の計5ページを表示
まとめ
今回は、GraphQLを使って、フロントエンド側でページネーションを作りました。普段はAPIに任せっきりになっているので、今回やってみて、結構ロジックがややこしいことを改めて感じました。
このような、基礎的な機能だけれど、結構ややこしいアルゴリズムが必要な部分が結構あると思うので、しっかり勉強して基礎は基礎らしくできるようになりたいです。
Discussion