フロントエンド:react バックエンドSpringboot RestApi　ページネーション実装

# 概要
SpringBootで作ったRestAPi(ページネーション)を
フロントエンド側で実装していきます。
内容としては　フロントエンドがメインです。
バックエンドはjsonのレスポンスぐらいしか出しません。


# 開発環境
開発環境
OS:windows10

バックエンド側：
IDE:IntelliJ Community
spring-boot-starter-parent 2.75
java　：　11

データベース
mysql:8.0.29
クライアントソフト: MySQL Workbench

フロントエンド：
IDE:VScode

├── @types/node@18.11.15
├── @types/react-dom@18.0.9
├── @types/react@18.0.26
├── axios@1.2.1
├── eslint-config-next@13.0.6
├── eslint@8.29.0
├── next-auth@4.18.7
├── next@13.0.6
├── react-bootstrap@2.7.0
├── react-dom@18.2.0
├── react@18.2.0
├── styled-components@5.3.6
├── styled-jsx@5.1.1
└── typescript@4.9.4
# ユースケース
以下gifアニメの動きを再現したいです。
![pagination _samole.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/03f6df7e-eb46-7fe5-c18e-ba0b2ea57bd2.gif)

杏林堂ネットスーパーの商品一覧を動かしている様子なんですけど


文字で記しておくと以下の振る舞いを仕様とします。
１．「前へ」をクリックすると現在のページ-1に遷移します。但しページが１のときは表示されなく押せません。
２．「次へ」をクリックすると現在のページ+1に遷移します。但しページが最終ページのときは表示されません。
３．数字部分を押すとそのページに遷移します。

# 実装
## バックエンド 
レスポンス

Postmanで検証する。
カテゴリーID1の　ページサイズ10にして、ページ番号５時の情報を取得します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/0abcf265-562f-0298-6df9-f749ee9364ae.png)


```json:response.data
{
    "content": [
        {
            "id": 50,
            "name": "PB ポークウィンナー 480g",
            "description": "des",
            "inStock": true,
            "categoryId": 1,
            "price": 298.0,
            "taxRate": 0.08,
            "discountPercent": 0.0,
            "image": "image"
        },
        {
            "id": 51,
            "name": "PB 皮なしウインナー 275g",
            "description": "des",
            "inStock": true,
            "categoryId": 1,
            "price": 258.0,
            "taxRate": 0.08,
            "discountPercent": 0.0,
            "image": "image"
        },
        {
            "id": 52,
            "name": "PB 特級あらびきポークウィンナー 260g",
            "description": "des",
            "inStock": true,
            "categoryId": 1,
            "price": 348.0,
            "taxRate": 0.08,
            "discountPercent": 0.0,
            "image": "image"
        },
        {
            "id": 53,
            "name": "PB チキンウインナー 281g",
            "description": "des",
            "inStock": true,
            "categoryId": 1,
            "price": 251.0,
            "taxRate": 0.08,
            "discountPercent": 0.0,
            "image": "image"
        },
        {
            "id": 54,
            "name": "PB 皮なしミニウインナー 80g",
            "description": "des",
            "inStock": true,
            "categoryId": 1,
            "price": 98.0,
            "taxRate": 0.08,
            "discountPercent": 0.0,
            "image": "image"
        }
    ],
    "pageNo": 5,
    "pageSize": 10,
    "totalElements": 55,
    "totalPages": 6,
    "last": true,
    "categoryId": 1,
    "categoryName": "食肉"
}

```



詳細はgithub参照
ドメイン

https://github.com/RYA234/springboot-ecshop-rest-api/tree/master/src/main/kotlin/com/example/restapi/domain/product

実装

https://github.com/RYA234/springboot-ecshop-rest-api/tree/master/src/main/kotlin/com/example/restapi/implement/product

## フロント側
３ステップを得て実装していきます。

### 1.とにかく動くページネーションを実装する。一つのファイルで動く物を作る
コンポーネント化は考慮してないですね
#### ソースコード
```tsx:paginationCheck.tsx
import { useState, useEffect, useCallback, EventHandler } from "react";

// ページネーション実装 その１
// 一つのファイルで実装している→要リファクタリング
// コンポーネント化の必要あり　RESTAPIは空にしている
import { MouseEventHandler } from "react";

export default function PaginationCheck() {
  const [currentPageNo,setCurrentPageNo] =  useState<number>(1);
  const totalPage: number = 10;
  const categoryId = 2;
  let paginationContent: JSX.Element[] = [];

  // 前へボタンを押したときの処理
  const backHandler = () => {
	setCurrentPageNo(currentPageNo-1);
	getPaginationProduct(currentPageNo)
  };

  // 次へボタンを押したときの処理
  const nextHandler = () => {
	setCurrentPageNo(currentPageNo+1);
	getPaginationProduct(currentPageNo)
  };

  // ページ番号を押したときの処理
  const selectNumberHandler = (pg: number) => {
	setCurrentPageNo(pg);
	getPaginationProduct(currentPageNo)
  };

  // RestApiを呼ぶ関数
  const getPaginationProduct = (page: number) => {
	// 実装前
	console.log(`RestApiの呼び出し処理は入っていません。categoryId:${categoryId} 現在のページ:${currentPageNo}`)
  }
  return (
    <>
	<h1>ページネーション検証</h1>
	<div>cssとreactのみで実装</div>
      <div className="Layout">
		{/* 前のページに戻る-ページ数が最初の場合は表示されない */}
	    <div className="Child">
        {currentPageNo != 1 ? 
            <button className="Font" onClick={backHandler}>前へ</button>
			:<div className="Font"> &emsp; &emsp;</div>  // 「前へ」ボタンが非表示でもレイアウトを維持するための処置。（何もしないと崩れる）
		}
		</div>
		{/*  即時関数を使って1からtotalNumberの数字を作成する。クリックすると押した番号のデータを取得するrestapiを呼ぶ関数を実行する */}
        {(() => {
          for (let i = 0; i < totalPage; i++) {
            paginationContent.push(
              <>  
                <button
				className= { currentPageNo== i+1 ?  'SelectedChild' :'Child'} 	  
				  onClick={()=>selectNumberHandler(i+1)}
                >
                  {i + 1}
                </button>
              </>
            );
          }
		  
          return <div>{paginationContent}</div>;
        })()}

		{/* 次のページへ進む-ページ数が最後の場合は表示されない */}
		<div className="Child">
        {currentPageNo != totalPage ? 
            <button className="Font" onClick={nextHandler}>次へ</button>
        	:<div className="Font"> &emsp; &emsp;</div>  // レイアウトを維持するために空欄を入れる
		}
	</div>
        &emsp;
      </div>
      <style jsx>{`
        // 大枠　要素を横並びにする
        .Layout {
          display: flex;
          flex-wrap: wrap;
          align-items: center;
        }
        // 子の幅 間隔を決定する
        .Child {
          flex-basis: auto;
          align-items: center;
          background-color: white;
          margin: 3px;
          border: none;
        }
		// 現在のページを明示するために背景色を変える　透明な青色とする
		.SelectedChild{
			flex-basis: auto;
			align-items: center;
			background-color: #1c5bcf85;
			margin: 5px;
			border: none;
		}
		
        // 文字のフォントサイズ　背景色設定
        .Font {
          align-items: center;
          background-color: white;
          text-align: center;
        }
        // マウスオーバー時の背景色は透明な灰色
        button.Font:hover {
          background-color: #1c5bcf85;
        }
		// 子要素のボタン感を無くすために　色と境界線を無くす
        button {
          background-color: white;
          border: none;
        }
      `}</style>
    </>
  );
}

```
#### 動作結果

![paginationCheck.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/913806e5-b45f-abfa-b9a1-0f9ef0d80f01.gif)


### 2. 1に基づいてコンポーネント化する　RestApiは呼ばない
####  ソースコード
```tsx:pagination.tsx
import { setHttpClientAndAgentOptions } from "next/dist/server/config";
import { useState, useEffect, useCallback, EventHandler, SetStateAction, Dispatch } from "react";
import { MouseEventHandler } from "react";

/**
 * @remarks ページネーション　コンポーネント
 * 			親コンポーネントに現在のページ数を渡します。
 * 			RestApiを呼ぶ関数は親コンポーネント側で行います。
 *  		           
 * @param totalPage: 総ページ数：固定値として使われる想定
 * @param pageNo:現在のページ番号を取得する useState[pageNo, setPageNo]の１つ目の引数
 * @param setPageNo:現在のページ番号を更新する useState[page,setPage]の２つ目の引数
 */
// 参考URL
// 
export default function Pagination(props:{totalPage:number ,pageNo:number, setPageNo:Dispatch<SetStateAction<number>>}) {
//   const [currentPageNo,setCurrentPageNo] =  useState<number>(1);
  let currentPageNo = 1;
  const totalPage: number = 10;
  let paginationContent: JSX.Element[] = [];

  // 前へボタンを押したときの処理
  const backHandler = () => {
	props.setPageNo( props.pageNo - 1)
  };

  // 次へボタンを押したときの処理
  const nextHandler = () => {
	props.setPageNo(props.pageNo + 1)
	console.log(props.pageNo);
  };

  // ページ番号を押したときの処理
  const selectNumberHandler = (pg: number) => {
	props.setPageNo(pg)
  };

  return (
    <>
      <div className="Layout">
		{/* 前のページに戻る-ページ数が最初の場合は表示されない */}
	    <div className="Child">
        {props.pageNo != 1 ? 
            <button className="Font" onClick={backHandler}>前へ</button>
			:<div className="Font"> &emsp; &emsp;</div>  // 「前へ」ボタンが非表示でもレイアウトを維持するための処置。（何もしないと崩れる）
		}
		</div>
		{/*  即時関数を使って1からtotalNumberの数字を作成する。クリックすると押した番号のデータを取得するrestapiを呼ぶ関数を実行する */}
        {(() => {
          for (let i = 0; i < props.totalPage; i++) {
            paginationContent.push(
              <>  
                <button
				className= { props.pageNo== i+1 ?  'SelectedChild' :'Child'} 	  
				  onClick={()=>selectNumberHandler(i+1)}
                >
                  {i + 1}
                </button>
              </>
            );
          }
		  
          return <div>{paginationContent}</div>;
        })()}

		{/* 次のページへ進む-ページ数が最後の場合は表示されない */}
		<div className="Child">
        {props.pageNo != props.totalPage ? 
            <button className="Font" onClick={nextHandler}>次へ</button>
        	:<div className="Font"> &emsp; &emsp;</div>  // レイアウトを維持するために空欄を入れる
		}
	</div>
        &emsp;
      </div>
      <style jsx>{`
        // 大枠　要素を横並びにする
        .Layout {
          display: flex;
          flex-wrap: wrap;
          align-items: center;
        }
        // 子の幅 間隔を決定する
        .Child {
          flex-basis: auto;
          align-items: center;
          background-color: white;
          margin: 3px;
          border: none;
        }
		// 現在のページを明示するために背景色を変える　透明な青色とする
		.SelectedChild{
			flex-basis: auto;
			align-items: center;
			background-color: #1c5bcf85;
			margin: 5px;
			border: none;
		}
		
        // 文字のフォントサイズ　背景色設定
        .Font {
          align-items: center;
          background-color: white;
          text-align: center;
        }
        // マウスオーバー時の背景色は透明な灰色
        button:hover {
          background-color: #1c5bcf85;
        }
		// 子要素のボタン感を無くすために　色と境界線を無くす
        button {
          background-color: white;
          border: none;
        }
      `}</style>
    </>
  );
}

```

```tsx:paginationCheckSecond.tsx

// ページネーション検証用ページ　その２
// コンポーネント化したページネーションを呼び出す
// SpringBootのRestApiは呼び出さない

import { useState } from "react";
import Pagination from "../../component/pagination";

export default function PaginationCheckSecond() {
	const [pageNo,setPageNo] =  useState<number>(1)
	const totalPage :  number = 4;
	let tempPageNo : number = 1;
	return(
		<>
			コンポーネント化したページネーションの検証ページ
			<Pagination totalPage={totalPage} pageNo={pageNo} setPageNo={setPageNo}/>
		</>
	)
}

```

#### 動作結果
1.の場合と同じ挙動ができております。

![paginationCheckSecond.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/3a747513-5167-e813-b151-f1df1025ade9.gif)

### 3. 2の処理にrestApiの処理を追加する。
####  ソースコード
親コンポーネント側にapiを実行する処理を加えます
```tsx:paginationCheckThird.tsx
import { SetStateAction, useEffect, useState } from 'react';
import Pagination from "../../component/pagination";
import * as productService from '../../service/productService';
import { Product } from '../../types/product/product';
import { ProductRequest } from '../../types/product/productRequest';

// ページネーション検証用ページ　その３
// コンポーネント化したページネーションを呼び出す
// SpringBootのRestApiを呼び出して使えるか確認する。

export default function PaginationCheckThird() {
	const [pageNo,setPageNo] =  useState<number>(1)
	const [pagingProduct, setPagingProduct] = useState<ProductRequest | undefined>()
	const totalPage :  number = 4;
	const pageSize : number = 10;
	let tempPageNo : number = 1;

	//  pageNo変更後にRestApiを呼ぶ関数が実行される
	useEffect(() =>{
		getProductByCategory()
	} ,[pageNo])
	
	// 
	const getProductByCategory =() =>{
		console.log(`pageNo  ${pageNo} .Rest api is executed.`)
		productService.getProductsByCategoryId(pageNo-1,pageSize,1)
		.then((response: { data: SetStateAction<ProductRequest | undefined>; })=>{
			setPagingProduct(response.data)
			console.log(pagingProduct)	
	  })}		
	return(
		<>
			コンポーネント化したページネーションの検証ページ　
			RESTAPIを使う		
				{
			pagingProduct?.content.map((product: Product) => {
				return (
					<div key={product.id}>
						{product.name} ,{Math.floor(product.price * (1 + product.taxRate))} 円
					</div>
				)
			})
			}
			
			
			<Pagination totalPage={pagingProduct?.totalPages} pageNo={pageNo} setPageNo={setPageNo}/>
		</>
	)
}

```

axiosを使ってapiを呼び出しています。
```tsx:productService.ts
import axios from 'axios';

export const getProductById = ( productId : number) =>{
	return axios.get(`http://127.0.0.1:5000/api/products/?id=`+ productId,
	{ headers: { 
		'Access-Control-Allow-Origin': '*',
	} } 
	);
};

export const getProductsByCategoryId = ( pageNo : number,pageSize:number, categoryId:number) =>{
	return axios.get(`http://127.0.0.1:5000/api/products?`
					+`pageNo=`+ pageNo
					+`&pageSize=`+ pageSize
					+`&category=`+ categoryId,
					);
};

```

#### 動作結果
左側：ブラウザ　右側：mySql workbench

![paginationCheckThird.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/f92c1971-3558-89bf-89ed-510749036c85.gif)


# 参考
杏林堂ネットスーパー

https://www.kyorindo-netsuper.jp/

実装方法

https://www.homepage-tukurikata.com/hp/pagination.html

子コンポーネントの値渡し

http://www.code-magagine.com/?p=13251