LoginSignup
2
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-01-11

概要

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

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

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

実装

バックエンド

レスポンス

Postmanで検証する。
カテゴリーID1の ページサイズ10にして、ページ番号5時の情報を取得します。
image.png

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参照
ドメイン

実装

フロント側

3ステップを得て実装していきます。

1.とにかく動くページネーションを実装する。一つのファイルで動く物を作る

コンポーネント化は考慮してないですね

ソースコード

paginationCheck.tsx
import { useState, useEffect, useCallback, EventHandler } from "react";

// ページネーション実装 その1
// 一つのファイルで実装している→要リファクタリング
// コンポーネント化の必要あり 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

2. 1に基づいてコンポーネント化する RestApiは呼ばない

ソースコード

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]の1つ目の引数
 * @param setPageNo:現在のページ番号を更新する useState[page,setPage]の2つ目の引数
 */
// 参考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>
    </>
  );
}

paginationCheckSecond.tsx

// ページネーション検証用ページ その2
// コンポーネント化したページネーションを呼び出す
// 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

3. 2の処理にrestApiの処理を追加する。

ソースコード

親コンポーネント側にapiを実行する処理を加えます

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';

// ページネーション検証用ページ その3
// コンポーネント化したページネーションを呼び出す
// 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を呼び出しています。

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

参考

杏林堂ネットスーパー

実装方法

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

2
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1