For

2021.12.28

React で現在のページと総アイテム数を入れるだけで表示できる汎用的なページネーションコンポーネントを作成する


ブログの記事一覧やECの商品一覧などで必ず実装することになるページネーション。ライブラリを使って簡単に実装することもできますが、スタイルを変更しようとしたり、表示する数字の幅を変更しようとすると意外と思うようにいかなかったりするので、汎用的なページネーションコンポーネントを作成したいと思います。

ページが切り替わるたびに当該ページで表示する記事、商品リストなどを API にリクエストする場合や、リクエストがそこまで重くならない想定ならば最初にすべてリクエストしてしまってあとはページネーションで表示を切り替えていく場合などあると思いますが、今回は前者の想定で作成します。

最終的にできあがるもの

TypeScript_____Pagination.tsx_____import * as React from 'react'
import { Link } from 'react-router-dom'
import { HiOutlineChevronDoubleLeft, HiOutlineChevronLeft, HiOutlineChevronDoubleRight, HiOutlineChevronRight } from 'react-icons/hi'


type _Props = {
    path: string
    currentPage: number
    pagesRange: number
    itemsPerPage: number
    allItemsNumber: number
    className?: string
}


const Pagination = ({ path, currentPage, pagesRange, itemsPerPage, allItemsNumber, className }: _Props): JSX.Element => {
    const maxPage = allItemsNumber % itemsPerPage == 0 ? allItemsNumber / itemsPerPage : Math.floor(allItemsNumber / itemsPerPage) + 1
    const generateListItems = () => {
        let listItems = []
        const listItemClass = 'w-36 h-36 rounded-6 flex justify-center items-center'
        const activeListItemClass = 'bg-theme-light text-white'
        const inactiveListItemClass = 'bg-white'
        const activeAnchorClass = 'w-100p h-100p flex justify-center items-center'
        if (currentPage != 1) {
            listItems.push(
                <>
                    <li key={`first`} className={`${listItemClass} ${inactiveListItemClass} mr-8`}>
                        <Link to={`${path}?page=${1}`} className={`${activeAnchorClass}`}>
                            <HiOutlineChevronDoubleLeft size={18} />
                        </Link>
                    </li>
                    <li key={`previous`} className={`${listItemClass} ${inactiveListItemClass} mr-8`}>
                        <Link to={`${path}?page=${currentPage - 1}`} className={`${activeAnchorClass}`}>
                            <HiOutlineChevronLeft size={18} />
                        </Link>
                    </li>
                </>,
            )
        }
        switch (true) {
            case maxPage <= pagesRange * 2 + 1:
                console.log(`log ::: pagination switch: 1 / maxPage: ${maxPage}`)
                for (let i = 1; i <= maxPage; i++) {
                    listItems.push(
                        <li key={i} className={`${i != 1 ? 'ml-8' : ''} ${listItemClass} ${i == currentPage ? activeListItemClass : inactiveListItemClass}`}>
                            {i == currentPage ? (
                                <p>{i}</p>
                            ) : (
                                <Link to={`${path}?page=${i}`} className={`${activeAnchorClass}`}>
                                    {i}
                                </Link>
                            )}
                        </li>,
                    )
                }
                break
            case maxPage >= pagesRange * 2 + 2 && currentPage <= pagesRange:
                console.log(`log ::: pagination switch: 2 / maxPage: ${maxPage}`)
                for (let i = 1; i <= (maxPage <= 4 ? maxPage : pagesRange * 2 + 1); i++) {
                    listItems.push(
                        <li key={i} className={`${i != 1 ? 'ml-8' : ''} ${listItemClass} ${i == currentPage ? activeListItemClass : inactiveListItemClass}`}>
                            {i == currentPage ? (
                                <p>{i}</p>
                            ) : (
                                <Link to={`${path}?page=${i}`} className={`${activeAnchorClass}`}>
                                    {i}
                                </Link>
                            )}
                        </li>,
                    )
                }
                break
            case maxPage >= pagesRange * 2 + 2 && currentPage >= maxPage - pagesRange + 1:
                console.log(`log ::: pagination switch: 3 / maxPage: ${maxPage}`)
                for (let i = maxPage - pagesRange * 2; i <= maxPage; i++) {
                    listItems.push(
                        <li
                            key={i}
                            className={`${i != maxPage - 6 ? 'ml-8' : ''} ${listItemClass} ${i == currentPage ? activeListItemClass : inactiveListItemClass}`}
                        >
                            {i == currentPage ? (
                                <p>{i}</p>
                            ) : (
                                <Link to={`${path}?page=${i}`} className={`${activeAnchorClass}`}>
                                    {i}
                                </Link>
                            )}
                        </li>,
                    )
                }
                break
            default:
                console.log(`log ::: pagination switch: 4 / maxPage: ${maxPage}`)
                for (let i = currentPage - pagesRange; i <= currentPage + pagesRange; i++) {
                    listItems.push(
                        <li
                            key={i}
                            className={`${i != currentPage - pagesRange ? 'ml-8' : ''} ${listItemClass} ${
                                i == currentPage ? activeListItemClass : inactiveListItemClass
                            }`}
                        >
                            {i == currentPage ? (
                                <p>{i}</p>
                            ) : (
                                <Link to={`${path}?page=${i}`} className={`${activeAnchorClass}`}>
                                    {i}
                                </Link>
                            )}
                        </li>,
                    )
                }
                break
        }
        if (currentPage != maxPage) {
            listItems.push(
                <>
                    <li key={`next`} className={`${listItemClass} ${inactiveListItemClass} ml-8`}>
                        <Link to={`${path}?page=${currentPage + 1}`} className={`${activeAnchorClass}`}>
                            <HiOutlineChevronRight size={18} />
                        </Link>
                    </li>
                    <li key={`last`} className={`${listItemClass} ${inactiveListItemClass} ml-8`}>
                        <Link to={`${path}?page=${maxPage}`} className={`${activeAnchorClass}`}>
                            <HiOutlineChevronDoubleRight size={18} />
                        </Link>
                    </li>
                </>,
            )
        }
        return listItems
    }
    return <ul className={`${className ? className : ''} flex flex-wrap`}>{...generateListItems()}</ul>
}


export default Pagination



コンポーネントの考え方

以下の値を受け取ることで表示できるように実装してみます。

・ページネーションを設置するページのパス( path )
・現在のページ数( currentPage )
・表示レンジ( pagesRange )
・1ページあたりの表示アイテム数( itemsPerPage )
・全アイテム数( allItemsNumber )
・コンポーネントの一番外側の要素に付与するクラス( className )

最後の「コンポーネントの一番外側の要素に付与するクラス」は、ページネーションに限らずコンポーネント作成における考え方となりますが、配置に関するスタイルはコンポーネント自身では持たないように設計し、配置する用のクラスはすべてコンポーネント呼び出し時に付与するものとします。

ページネーションの構成要素


今回実装するページネーションは、上記の5つの要素から構成されているので、順番に実装していきたいと思います。
最終的には以下のようなかたちでページネーションのリストを返します。

TypeScript_____Pagination.tsx_____return <ul className={`${className ? className : ''} flex flex-wrap`}>{...generateListItems()}</ul>


そのため generateListItems() という関数でページネーションのリストを返せるように設計します。

①First / Previous のページネーション

ここは非常にシンプルで、現在のページ数が「1」でなければ表示しておきます。細かく表示をハンドリングしたい場合はここの条件の中でカスタムしてみてください。

TypeScript_____Pagination.tsx_____        if (currentPage != 1) {
            listItems.push(
                <>
                    <li key={`first`} className={`${listItemClass} ${inactiveListItemClass} mr-8`}>
                        <Link to={`${path}?page=${1}`} className={`${activeAnchorClass}`}>
                            <HiOutlineChevronDoubleLeft size={18} />
                        </Link>
                    </li>
                    <li key={`previous`} className={`${listItemClass} ${inactiveListItemClass} mr-8`}>
                        <Link to={`${path}?page=${currentPage - 1}`} className={`${activeAnchorClass}`}>
                            <HiOutlineChevronLeft size={18} />
                        </Link>
                    </li>
                </>,
            )
        }


②〜④現在のページとその前後のページ

この部分は以下の4パターンに分解して実装しました。

TypeScript_____Pagination.tsx_____        switch (true) {
            case maxPage <= pagesRange * 2 + 1:
                console.log(`log ::: pagination switch: 1 / maxPage: ${maxPage}`)
                for (let i = 1; i <= maxPage; i++) {
                    listItems.push(
                        <li key={i} className={`${i != 1 ? 'ml-8' : ''} ${listItemClass} ${i == currentPage ? activeListItemClass : inactiveListItemClass}`}>
                            {i == currentPage ? (
                                <p>{i}</p>
                            ) : (
                                <Link to={`${path}?page=${i}`} className={`${activeAnchorClass}`}>
                                    {i}
                                </Link>
                            )}
                        </li>,
                    )
                }
                break
            case maxPage >= pagesRange * 2 + 2 && currentPage <= pagesRange:
                console.log(`log ::: pagination switch: 2 / maxPage: ${maxPage}`)
                for (let i = 1; i <= (maxPage <= 4 ? maxPage : pagesRange * 2 + 1); i++) {
                    listItems.push(
                        <li key={i} className={`${i != 1 ? 'ml-8' : ''} ${listItemClass} ${i == currentPage ? activeListItemClass : inactiveListItemClass}`}>
                            {i == currentPage ? (
                                <p>{i}</p>
                            ) : (
                                <Link to={`${path}?page=${i}`} className={`${activeAnchorClass}`}>
                                    {i}
                                </Link>
                            )}
                        </li>,
                    )
                }
                break
            case maxPage >= pagesRange * 2 + 2 && currentPage >= maxPage - pagesRange + 1:
                console.log(`log ::: pagination switch: 3 / maxPage: ${maxPage}`)
                for (let i = maxPage - pagesRange * 2; i <= maxPage; i++) {
                    listItems.push(
                        <li
                            key={i}
                            className={`${i != maxPage - 6 ? 'ml-8' : ''} ${listItemClass} ${i == currentPage ? activeListItemClass : inactiveListItemClass}`}
                        >
                            {i == currentPage ? (
                                <p>{i}</p>
                            ) : (
                                <Link to={`${path}?page=${i}`} className={`${activeAnchorClass}`}>
                                    {i}
                                </Link>
                            )}
                        </li>,
                    )
                }
                break
            default:
                console.log(`log ::: pagination switch: 4 / maxPage: ${maxPage}`)
                for (let i = currentPage - pagesRange; i <= currentPage + pagesRange; i++) {
                    listItems.push(
                        <li
                            key={i}
                            className={`${i != currentPage - pagesRange ? 'ml-8' : ''} ${listItemClass} ${
                                i == currentPage ? activeListItemClass : inactiveListItemClass
                            }`}
                        >
                            {i == currentPage ? (
                                <p>{i}</p>
                            ) : (
                                <Link to={`${path}?page=${i}`} className={`${activeAnchorClass}`}>
                                    {i}
                                </Link>
                            )}
                        </li>,
                    )
                }
                break
        }


⑤Next / Last のページネーション

こちらは①同様にシンプルに現在のページ数が最大ページ数未満だったら表示することにします。

TypeScript_____Pagination.tsx_____        if (currentPage != maxPage) {
            listItems.push(
                <>
                    <li key={`next`} className={`${listItemClass} ${inactiveListItemClass} ml-8`}>
                        <Link to={`${path}?page=${currentPage + 1}`} className={`${activeAnchorClass}`}>
                            <HiOutlineChevronRight size={18} />
                        </Link>
                    </li>
                    <li key={`last`} className={`${listItemClass} ${inactiveListItemClass} ml-8`}>
                        <Link to={`${path}?page=${maxPage}`} className={`${activeAnchorClass}`}>
                            <HiOutlineChevronDoubleRight size={18} />
                        </Link>
                    </li>
                </>,
            )
        }


これでひととおり実装完了です。
付与しているクラスはすべて TailwindCSS のクラスなので、スタイリング以外は作用していません。
スタイルを変えたい場合は不要な TailwindCSS のクラスを消して適宜変更してもらえればすぐに使えるかと思います。