フロントNext.js　バックSpringBoot　カテゴリーメニューを実装する

# 概要
BtoC向けECショップのクローンサイトを作っています。
今回はカテゴリーメニューを実装してカテゴリー毎に商品表示できるようにします。
前回の記事の続きでページ左側にカテゴリーメニューを追加します。

[フロントNext.js バックSpringBoot　商品一覧から商品を選択して買い物カゴに加える動きを実装する](https://qiita.com/RYA234/items/0297aa890cf2d32b3213)

# ユースケース
杏林堂ネットスーパーを参考にします。

![product_category.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/4643cecb-4bff-6649-bdb8-f9aaaf71c996.gif)

1.カテゴリーの項目を選択する
２．選択したカテゴリーの商品群が表示される。

※カテゴリーについて
カテゴリーには親カテゴリーと子カテゴリーがある。
親カテゴリーはクリックできない。子カテゴリーのみ商品の切り替えができる
孫カテゴリーは対応していない


# 開発環境 
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

# 実装


## 作成したファイル一覧
ファイル名|説明
------|------
index.tsx|既存のファイル。カテゴリーに関するStateを２つを追加
categoryMenu.tsx|カテゴリーメニューのコンポーネント 子カテゴリーの動きはCSSのみで実現
category.d.ts|カテゴリー単体の型定義ファイル
categoryService.ts|Api処理が入っている。axiosを利用
productView.tsx|既存のファイル。カテゴリーが変化したときに商品情報を更新するファイルを追加



## 実装の方針
useEffectを使って、初回時にカテゴリー情報を取得する。

カテゴリーを切り替える時の動き
１．カテゴリーを押すとCategoryId(UseStateになっている)の値が更新される。
２．CategoryIdの値が変化したら商品情報を再取得する。（useEffectを利用）
３．取得した商品情報はproductResponseに代入する。
　　productResponseはState化されているので、再レンダリングされる。

※CategoryIdはcontextを使って他のコンポーネント渡している。(index.tsxでcreateContextをしてる)



## コード部分

### index.tsx

```diff_tsx:index.tsx
import { createContext, useState,useEffect } from "react";
import { CartItemResponse } from "../../../types/cartItem/cartItemResponse";
import { Customer } from "../../../types/customer";
import { ProductResponse } from "../../../types/product/productResponse";
import * as CustomerService from "../../../service/customerService";
import * as CartItemService2 from "../../../service/cartItemServiceV2";
+import * as CategoryService from "../../../service/categoryService";
import ProductView from "./productView";
import CartItemView from "./cartItemView";
+import CategoryMenu from "./categoryMenu";
+import { Category } from "../../../types/category/category";
+import * as productServiceV2 from '../../../service/productServiceV2';

export const mainContext  = createContext(0);
// 商品一覧機能と買い物カゴ機能をつなげる方法を確認するページ
// http://127.0.0.1:3000/develop/cartItemAndProduct
export default function Index() {
  // 商品情報のhttpレスポンス
  const [productResponse, setProductResponse] = useState<ProductResponse>();
  // 買い物カゴ情報のhttpレスポンス
  const [cartItemsResponse, setCartItemsResponse] = useState<CartItemResponse>();
  // カテゴリー情報のhttpレスポンス
+  const [categoryResponse,setCategoryResponse] = useState<Category[]>([]);
+  const[categoryId,setCategoryId] = useState<number>(1);

 

  // 初回時　ログインしてユーザーの買い物かご情報を取得する。
 useEffect(() => {
	loginAndGetCartItem();
		},[]);

  // ログインするときに必要なユーザー情報
  const loginRequestBody: Customer = {
    email: "aaaba@gmail.com",
    password: "test",
    append: function (arg0: string, inputEmail: string): unknown {
      throw new Error("Function not implemented.");
    },
  };

//   ログインしてカートアイテムの情報を取得する
  const loginAndGetCartItem = async () => {
    const response = await CustomerService.signIn2(loginRequestBody);
    localStorage.setItem("accessToken", response);
    await getCartItem();
+	await getCategory();
  };

  // カテゴリーの情報を取得する
+  const getCategory = async () => {
+	const response = await CategoryService.getCategories();
+	await setCategoryResponse(response);
+  }
  // ユーザーのカート情報を取得する
  const getCartItem = async () => {
    let response: CartItemResponse = await CartItemService2.getCartItems2(
      localStorage.getItem("accessToken") as unknown as string
    );
+	await  setCartItemsResponse(response);
  };


  return(
  	<div className="Layout">
+		<div className="CategoryMenuPosition">
+			<mainContext.Provider value={{ categoryResponse,setCategoryId}}>
+				<CategoryMenu/>
+			</mainContext.Provider>
+		</div>
		<div className="ProductViewPosition">
+			<mainContext.Provider value={{ productResponse, +setProductResponse,setCartItemsResponse,categoryId }}>
				<ProductView />
			</mainContext.Provider>
		</div>

		<div className="CartItemViewPosition">
		<mainContext.Provider value={{ cartItemsResponse, setCartItemsResponse }}>
			<CartItemView />
		</mainContext.Provider>

		</div>

		<style jsx>{`
			.Layout{
				display:flex;
				flex-direction:row;
			}				
+			.CategorymenuPosition{
+				flex-basis:400px;
			}
			// ProductViewは中心に置く
			.ProductViewPosition{
				display:flex;
				flex-basis:1000px;
				flex-wrap:wrap;					
				// width:500px;

			}
			// CartItemViewは右側に置く
			.CartItemViewPosition{
				//align-self:stretch;
			}
		`}</style>
  
  	</div>
	);
}

```

### categoryMenu.tsx

```diff_tsx:categoryMenu.tsx
import test from "node:test";
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useContext, useEffect, useState } from "react";
import { Category } from "../../../types/category/category";
import { CategoryResponse } from '../../../types/category/categoryResponse';
import { mainContext } from './index';

// todo リファクタリング必要


// カテゴリーメニューのコンポーネント
export default function CategoryMenu() {
	const {categoryResponse,setCategoryId} : any = useContext(mainContext);

	const handlerClick = (onClickedCategoryId: number) => {
		console.log(onClickedCategoryId)
		setCategoryId(onClickedCategoryId);
	}	
	return (
		<div>
			カテゴリー覧
			<nav>
			<ul>
				{
					categoryResponse.map((category: Category) => {
						// 親カテゴリーだった場合表示する。
						if (category.parent == null)
							return (
								// eslint-disable-next-line react/jsx-key
								<li className='mainMenu'>
									<a href="#">{category.name}</a>					
									<ul>
										{
											// 子カテゴリーを表示する。
											 category.children.map(count => {
												const  title  = categoryResponse.at(count).name;
												return(
										
													<li key={count}>
														<button className="ButtonLayout"  onClick={() => handlerClick(categoryResponse.at(count).id)} >{title}</button>	
													</li>			
												)
											 })
										}
									</ul>
									
								</li>
							);
					})
				}
			</ul>
			</nav>
			<>

			</>
			<style jsx>{`	
				// 親カテゴリの設定 リストの・を消す。
				nav ul{		
					list-style: none;
				}

				// 子カテゴリのデフォルトのcss設定
				// hiddenで非表示にしてleftで位置崩れを防いでいる
				nav ul li ul{
					position:absolute;
					visibility: hidden;
					left:0px;
				}

				// 親カテゴリをマウスオーバーしたときに、子カテゴリを表示する。				
				nav  li.mainMenu:active > ul,
				nav  li.mainMenu:hover > ul,
				{
					position:absolute;
					z-index:5;
					visibility: visible;
					list-style: none;
					// ↓↓使うコンポーネントによっては、修正の必要があるかも…
					padding: 20px;
				    margin: 20px 10px 100px 0px;
					top:0px;
				    left:100px;
					width:180px;
					background-color: white;					
					// ↑↑使うコンポーネントによっては、修正の必要があるかも…
				}
				.ButtonLayout{
					background-color: white;
					border: none;
				}
			`}</style>
		</div>
	)
}


```

### category.d.ts

```diff_ts:category.ts
/**
 * @remarks 商品のカテゴリーを表す型です。
 * 
 * @auther RYA234 
 * 
 */

export  type Category = {
	/** MySQL側のIDです。*/
	id:number
	/** リンクとしてつかうので英語が入ります。  */
	alias:string
	/**　html上で表示されるカテゴリー名称です。日本語が入ることを想定してます。 */
	name:string
	/** サブカテゴリーです。サブカテゴリーを持つ場合対応するidが入ります。サブカテゴリーがない場合はnullが入ります。 */
	children:number[];
	// 親カテゴリーのidが入ります。親が存在する場合はそのidが入ります。親が存在しない場合はnullが入ります。
	parent : number;
}
```

### categoryService.ts

```diff_tsx:categoryService.ts
import axios, { AxiosResponse } from 'axios';
import { Category } from '../types/category/category';
import { CategoryResponse } from '../types/category/categoryResponse';

 export  const getCategories = async () =>{
	return await axios.get(`http://127.0.0.1:5000/api/category/all`)
	.then((response)=>{
		return response.data as AxiosResponse<Category[]>;
	})
	.catch((error)=>{
		return error;
	});
};

```

### productView.tsx

```diff_tsx:productView.tsx
import { ProductResponse } from '../../../types/product/productResponse';
import { createContext, SetStateAction, useContext, useEffect, useState } from 'react';
import { mainContext } from '.';
import * as productServiceV2 from '../../../service/productServiceV2';
import { Product } from '../../../types/product/product';
import ProductContentV2 from './productContentV2';
import PaginationV2 from './paginationV2';


export default function ProductView(){
	// 商品情報のhttpレスポンス
+	const {productResponse,setProductResponse,categoryId}:any = useContext(mainContext)
	// ページ番号
	const [pageNo,setPageNo] = useState<number>(1)
	
	// １ページあたりの商品数
	const pageSize : number = 10;
	
-	// 検証のためにカテゴリーIDを1に固定
-	// const categoryId : number = 1;
	
	// pageNoが変わるとgetProductByCategoryが実行される
	useEffect(() => {
		getProductByCategory()
	},[pageNo])
	
+	// categoryIdが変わるとgetProductByCategoryが実行されてpageNoを1に戻す。
+	useEffect(() => {
+		getProductOnChangeCategory()
+	},[categoryId])
	

	// ページネーション化されたカテゴリー毎の商品情報たちを取得する。
	const getProductByCategory = async () => {
		const response : ProductResponse= await productServiceV2.getProductsByCategoryId2(pageNo - 1,pageSize,categoryId)
+		await setProductResponse(response)
+	}

+	const getProductOnChangeCategory = async () => {
+		const response : ProductResponse= await productServiceV2.getProductsByCategoryId2(0,pageSize,categoryId)
+		await setPageNo(1);
+		await setProductResponse(response)
	}
	return(
		<>
			<div className="Layout">
			{
				productResponse?.content.map((product: Product,index:number)=>{
					return(
						<div className="Child" key={index}>
								<ProductContentV2 productName={productResponse.content[index].name} priceWithoutTax={productResponse.content[index].price} priceIncludingTax={(productResponse.content[index].price * (1 + productResponse.content[index].taxRate)).toFixed() as string} imageURL={'/sampleProduct1.JPG'} id={productResponse.content[index].id}/>
						</div>
					)			
				})
			}
			</div>
			<div className="PaginationPosition">
			      	<PaginationV2 totalPage={productResponse?.totalPages as number} pageNo={pageNo} setPageNo={setPageNo}/>
			</div>
		<style jsx>{`				
				// 大枠
				.Layout{
					display:flex;
					flex-wrap:wrap;					
					background-color:white;
				}
				// 子の幅を指定 これしないとレイアウトが崩れる
				.Child{
					flex-basis:200px;
					//align-self:stretch;
				}
				.PaginationPosition{
					margin:auto;
					width:50%
				}
			`}</style>		
		</>
	)
}
```









# 実装後の確認
## カテゴリーボタンを押して商品一覧が切り替わることを確認
下gifアニメはその時の様子です。初期のカテゴリーは肉から魚、野菜と切り替えています。
![product_category_sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/5fa5b276-2a5d-ead8-d059-52b8130a97f9.gif)





# 参考

参考にしたサイト（杏林堂ネットスーパー）

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


カテゴリーメニューの実装方法　cssのみで実装できる（当時はcssの擬似クラス（hoverとか）がわからなくて質問した）

https://qiita.com/RYA234/questions/90a4425c17ecd1defefe