React UIとロジック部分を分離する-題材はページネーション

# 概要

今回は自作したページネーションコンポーネントをリファクタリングします。
現状ではUIとロジック部分が一つのコンポーネントに集中しており、それを解消するためリファクタリングします。

# 開発環境
フロントエンド：
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|paginationコンポーネントを呼び起こすコンポーネント
pagination.tsx|ページネーションのコンポーネント(UI+ロジック)

Paginationコンポーネントの責務にロジック部分とUI部分が集中しています。これを改善します。

リファクタリング後
ファイル名|説明
-------------|------
index.tsx|paginationコンポーネントを呼び起こすコンポーネント　変化なし
paginationV2.tsx|ページネーションのUI部分 usePaginationコンポーネントを参照する。
usePagination.ts|ページネーションのロジック部分

既存のpaginationコンポーネントからロジック部分を抽出して、ロジック部分を担当するusePaginationコンポーネントを新規作成します。
usePaginationはReactの**CustomHook**を使っています。

# コード部分
## リファクタリング前
### index.tsx
```diff_tsx:index.tsx
import { useState } from 'react';
import PaginationV2 from '../../../component/product/paginationV2';

// http://127.0.0.1:3000/develop/productContent/paginationRefact
export default function PaginationRefact(){
    
    const [pageNo,setPageNo] =  useState<number>(1)
    const totalPage : number = 10;
    return(
       <div>
        <PaginationV2 totalPage={10} pageNo={pageNo} setPageNo={setPageNo}/>
       </div> 
    )
}
```

### pagination.tsx
```diff_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]の２つ目の引数
 */
export default function Pagination(props:{totalPage:number ,pageNo:number, setPageNo:Dispatch<SetStateAction<number>>}) {
-  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>
    </>
  );
}

```


## リファクタリング後

### paginationV2.tsx
```diff_tsx:pagination.tsx
import { setHttpClientAndAgentOptions } from "next/dist/server/config";
import { useState, useEffect, useCallback, EventHandler, SetStateAction, Dispatch } from "react";
import { MouseEventHandler } from "react";
import usePagination from "./usePagination";

/**
 * @remarks ページネーション　コンポーネント
 * 			親コンポーネントに現在のページ数を渡します。
 * 			RestApiを呼ぶ関数は親コンポーネント側で行います。
 *  		           
 * @param totalPage: 総ページ数：固定値として使われる想定
 * @param pageNo:現在のページ番号を取得する useState[pageNo, setPageNo]の１つ目の引数
 * @param setPageNo:現在のページ番号を更新する useState[page,setPage]の２つ目の引数
 */
// UIとロジック部分を分離済み
// UIはPaginationV2が担当
// ロジックはusePaginationが担当
export default function PaginationV2(props:{totalPage:number ,pageNo:number, setPageNo:Dispatch<SetStateAction<number>>}) {
//   let currentPageNo = 1;
//   const totalPage: number = 10;
+  const{paginationContent, backHandler, nextHandler, selectNumberHandler}=usePagination(props);
  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>
    </>
  );
}

```

### usePagination.tsx
```diff_tsx:pagination.tsx
import { Dispatch, SetStateAction } from "react";

export default function usePagination(props:{totalPage:number ,pageNo:number, setPageNo:Dispatch<SetStateAction<number>>}){
    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{paginationContent, backHandler, nextHandler, selectNumberHandler}
}
```


# 感想
楽ですね。Hookを自作するって難しそうな思い込みがありましたが実際にやってみるとかなり楽です。
これでロジック部分のテストコードも書きやすくなります。


# 参考

わかりやすい

https://qiita.com/pikimaru/items/5d0191244251c2d2a34a

りあクト 10-1-5 Custom Hookでロジックを再利用しやすくする

プロダクトコード作ったときの記録

https://qiita.com/RYA234/items/53461419199f69f69538#%E3%82%BD%E3%83%BC%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%89-2