3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-03-13

概要

今回は自作したページネーションコンポーネントをリファクタリングします。
現状では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

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

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つ目の引数
 */
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

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

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を自作するって難しそうな思い込みがありましたが実際にやってみるとかなり楽です。
これでロジック部分のテストコードも書きやすくなります。

参考

わかりやすい

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

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

3
0
0

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
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?