📑

Gatsbyでページネーションコンポーネント開発

2022/02/23に公開

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にlimitskipというプロパティが用意されていましたので、これらを使って、ページ数に応じて必要な記事を取得します。

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ページ以上あれば、endcurrent_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