【React】ライブラリを使わずにページネーションを実装する方法

## はじめに
備忘録。Reactを使って、ライブラリを使わずにページネーション機能を実装する手順を書いていきます。

ここではいくつかの記事を載せられるWebアプリを想定して進めますが、記事のデータは「[{JSON} Placeholder](https://jsonplaceholder.typicode.com/)」と言うサイトからサンプルデータを引っ張ってくる事にします。

* __実装するページネーション機能は主に以下の様にします。__


 * 1ページに表示させる記事数は５つ
 * 「次へ」ボタンと「前へ」ボタンを設置する
 * 基本的に10ページ分の目次を表示させる
 * 7ページ以降は、ページネーションバーでその番号が真ん中に表示される様にする

<br>

* __実装結果は以下の様な感じです（スタイリングは省略）__

　　●6ページ目を表示させている時

![スクリーンショット 2020-10-09 13.23.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606750/05f17d68-0d30-a297-bfe8-d66f3beeab47.png)

　　●7ページ目を表示させている時

![スクリーンショット 2020-10-09 13.24.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606750/14b4625c-d0cf-e64d-5d3e-2562dee6ebfb.png)


## 参考ソース

* [Simple Frontend Pagination | React](https://www.youtube.com/watch?v=IYCa1F-OWmk)
* [SEO Friendly Pagination Component With React like google](https://medium.com/mddanishyusuf/seo-friendly-pagination-component-with-react-like-google-95a4db266d0c)

※上記の資料では、無限に増えるデータについては触れられていないかったのでこの記事ではそこを補ってます。

## 環境

* React（React Hooksを使うのでv.16.8以降）
* react-router-dom
* axios

## 実装手順

#### 1.Reactセットアップ

```
$ npx create-react-app pagination-app
```

```
$ yarn add react-router-dom axios
```

#### 2. App.js内の記述

```react:src/App.js
import React from 'react';
import './App.css';
import {BrowserRouter as Router, Route, Switch, Redirect} from 'react-router-dom'
import Posts from './components/Posts';



function App() {

  return (
    <Router>
      <Switch>
        <Route 
          exact path={"/Articles/:article_id"} render={props => (
                <Home {...props} articleId={props.match.params.article_id} />
              )} />
        <Redirect from="/" to="/Article/1" />
      </Switch>
    </Router>
  );
}

export default App;
```

> __ここでは全ての記事をレンダルングするHomeというコンポーネントに、パラメータとして`article_id`を渡していますが、これは`「現在表示されているページ」`のパラメータとして扱います。__

#### 3. srcディレクトリ配下にHome.jsコンポーネントを作成する。

```react:src/Home.js
import React, {useState, useEffect} from 'react'
import axios from 'axios'
import Pagination from './Pagination'  // srcディレクトリ配下にPaginationコンポーネントを作成する


function Home({ articleId }) {
    const [articles, setArticles] = useState([])
    const [currentPage] = useState(articleId) // 現在のページのパラメータ
    const [articlesPerPage] = useState(5)  // 1ページ内に表示させる記事数


    // 記事の取得
    useEffect(() => {
        const getArticles = () => {
          axios
            .get("https://jsonplaceholder.typicode.com/posts")
            .then( res => setArticles(res.data) )
        }
        getArticles()
    }, [])


    // 1ページ内に表示させる記事の取得。以下のコードは後で解説
    const indexOfLastArticle = currentPage * articlesPerPage
    const indexOfFirstArticle = indexOfLastArticle - articlesPerPage
    const currentArticles = articles.slice(indexOfFirstArticle, indexOfLastArticle)

    return (
        <React.Fragment>
          <div>
            <Pagination
              articlesPerPage={articlesPerPage}  //1Pに表示する記事の数
              totalArticles={articles.length}   // 記事の総数数
              currentPage={currentPage}  // 現在のページ
            />
            <ul>
              {currentArticles.map(article => (
                  <li key={article.id}>{article.id}： {article.title}</li>
              ))}
            </ul>
            <Pagination
              articlesPerPage={articlesPerPage}  //1Pに表示する記事の数
              totalArticles={articles.length}   // 記事の総数
              currentPage={currentPage}  // 現在のページ
            />
          </div>
        </React.Fragment>
    )
}

export default Home
```

>#### ● axiosでアクセスしているURL （https://jsonplaceholder.typicode.com/posts） 先のデータはこんな感じです。

>> ![スクリーンショット 2020-10-09 12.46.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606750/74518d43-ca64-f71f-b61d-f718d8017a7c.png)

> #### ● 1ページ内に表示させる記事の取得

>>```
    const indexOfLastArticle = currentPage * articlesPerPage
    const indexOfFirstArticle = indexOfLastArticle - articlesPerPage
    const currentArticles = articles.slice(indexOfFirstArticle, indexOfLastArticle)
```

>> 上記のコード解説

>>* `indexOfLastArticle` ： `現在表示されているページのIDに、表示させる記事数を掛けることで、そのページの最後の記事のIDを取得`
* `indexOfFirstArticle` ： `最後の記事IDから表示させる記事数を引くことで最初の記事のID取得`
* `currentArticles` ： `最初の記事IDから最後の記事IDの直前の数の記事を取得`
* `【例】`
 * `currentPage = 3, articlesPerPage = 5だと、indexOfLastArticle = 15`
 * `indexOfLastArticle=15, articlesPerPage = 5で、indexOfFirstArticle = 10`
 * そして最終的に`articles.slice(10, 15)`となるので、`3ページ目ではIDが10, 11, 12, 13, 14の記事が取得される事になる`

>> __※自身のアプリケーションでここを実装する際はarticlesを配列型であるかどうかを確認してください。例えばオブジェクト型になっていたりするとslice()メソッドを使った場合にnot a functionエラーが出てしまいます。__

> #### ● Paginationコンポーネントについてはこの後記述します。 

#### 4.srcディレクトリ配下にPagination.jsコンポーネントを作成する。

```react:src/Pagination.js
import React from 'react'

function Pagination({ articlesPerPage, totalArticles, currentPage }) {

    const makePaginationHref = () => {
        const totalPages = []   // ページの総数を取得
        for (let i = 1; i <= Math.ceil(totalArticles / articlesPerPage); i++) {  // 記事総数÷5=ページの総数として取得
            totalPages.push(i)
        }

        const activePage = parseInt(currentPage)  // 現在のページ
        var startIndex, endIndex        // 目次の最初と最後
        let pages = totalPages.length   // ページの総数を数値で取得
        if (pages <= 10) {    // ページ総数が１０以下の場合
            startIndex = 1    // 最初の目次=1
            endIndex = pages  // 最後の目次=ページの最後のID（2〜10のどれか）
        } else {
            if (activePage <= 6) {  // 現在のページが６以下の場合
                startIndex = 1      // 最初の目次=1
                endIndex = 10       // 最後の目次=10
            } else if (activePage + 4 >= pages) {  // 現在のページがページ総数のラスト4に差し掛かった場合
                startIndex = pages - 9  // 最初の目次=ページ総数の-9
                endIndex = pages        // 最後の目次=ページの最後のID
                //  例えば総ページ数 = 25で、 現在のページが21の場合
                //  【 前へ 16 17 18 19 20 [21] 22 23 24 25 】
            } else {  // それ以外（現在のページが7ページ以降の場合）
                startIndex = activePage - 5  // 最初の目次=現在のページの-5
                endIndex = activePage + 4    // 最後の目次=現在のページの+5
                //  例えば現在のページが７の場合は
                //  【 前へ 2 3 4 5 6 [7] 8 9 10 11 次へ 】  となる
            }
        }

        const pageNumbers = []  // ページネーションの目次に使用する配列
        if (startIndex > 1) {  // 最初の目次が1以上なら「前へ」を表示させる
            pageNumbers.push(
                <a href={`/Articles/` + (activePage - 1)} key="1">
                    <span>≦前へ</span>
                </a>
            )
        }

        for (let i = startIndex; i <= endIndex; i++) {  // 最初の目次から最後の目次までのIDを取得
            pageNumbers.push(
                <a href={"/Articles/" + i} key={i} className={activePage === i ? "active" : ""}>  // "active"というクラス名にスタイリングを記述する事で現在のページを目立たせることができる。
                    <span>{i}</span>
                </a>
            )
        }

        if (endIndex < pages) {  // 最後の目次がページ総数以下の場合「次へ」を表示
            pageNumbers.push(
                <a href={"/Articles/" + (activePage + 1)} key={pages}>
                    <span>≧次へ</span>
                </a>
            )
        }
        return pageNumbers  // ページネーションの配列を返す
    }


    return (
        <nav>
            {/* ページネーションのレンダリング */}
            <h2>{makePaginationHref()}</h2>
        </nav>
    )
}

export default Pagination
```

## 最後に
以上で、ページネーションの実装が完了しました！
何らかの不備や、もっと良いアイデアなどあればご教示頂けると幸いです🙇‍♂️