React, NextでPaginationを作成

## はじめに
大学の単位が取れれば来年から正社員です。
つまりまだ学生のひよっこです。
自力で考えたコードなので、強強な方が見ればそれはよくないよ!!みたいな書き方をしている可能性もあります。
## 前提
必要なデータを全件取得しstateに配列で保存している。
そこから任意の個数で表示し、ページネーションで表示を切り替えることを想定しています。
環境はNext14 x TypeScriptで作成しています
## 今回使用するコード
```tsx
"use client";

import { useState } from "react";

const items = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const SHOW_COUNT = 3;

export default function Home() {
  const [offset, setOffset] = useState(1);
  const [results, setResults] = useState(items);

  const nextPage = () => {
    const necessaryButtonCount = Math.ceil(results.length / showCount);
    if (necessaryButtonCount === offset) return;
    setOffset((prevState) => prevState + 1);
  };

  const prevPage = () => {
    if (offset === 1) return;
    setOffset((prevState) => prevState - 1);
  };

  const changeOffset = (num: number) => {
    setOffset(num);
  };

  return (
    <div>
      <p>結果</p>
      {getShowData(results, offset, SHOW_COUNT).map((result) => {
        return <p key={result}>{result}</p>;
      })}
      <div className="flex">
        <button
          className="border border-solid border-[#333]"
          onClick={prevPage}
        >
          前へ
        </button>
        {getButtonCount(results, SHOW_COUNT).map((count) => (
          <button
            key={count}
            className="rounded-full border border-solid border-black
          w-5 h-5 ml-1 flex justify-center items-center"
            onClick={() => changeOffset(count)}
          >
            {count}
          </button>
        ))}
        <button
          className="border border-solid border-[#333] ml-3"
          onClick={nextPage}
        >
          次へ
        </button>
      </div>
    </div>
  );
}

function getShowData<T>(results: T[], offset: number, showCount: number): T[] {
  const firstArg = (offset - 1) * showCount;
  const secondArg = offset * showCount;
  return results.slice(firstArg, secondArg);
}

function getButtonCount<T>(results: T[], showCount: number): number[] {
  const necessaryButtonCount = Math.ceil(results.length / showCount);
  
  let resultCount = [];
  for (let i = 0; i < necessaryButtonCount; i++) {
    resultCount.push(i + 1);
  }
  return resultCount;
}
```

## 解説
```ts
// 結果として表示したいコンテンツ(通常はAPI経由でデータを取得します)
const items = [1, 2, 3, 4, 5, 6, 7, 8, 9];

// 1ページあたりの表示数
const SHOW_COUNT = 3;
```

```ts
// 現在表示しているページ番号
// offsetを変更することで表示されるコンテンツが変わります
const [offset, setOffset] = useState(1);

// 表示したいコンテンツをstateで管理しています
// 通常の運用だとuseEffectで初回マウント時にAPIから取得した情報を入れることになります
const [results, setResults] = useState(items);

// 次へボタンのonClickに渡す関数
// offsetが存在しないページ数を参照しないようにif文がtrueの時は即時returnします
const nextPage = () => {
  const necessaryButtonCount = Math.ceil(results.length / showCount);
  if (necessaryButtonCount === offset) return;
  setOffset((prevState) => prevState + 1);
};

// 前へボタンのonClickに渡す関数
// 1ページよりも後ろのページは存在しないのでif文で処理します
const prevPage = () => {
  if (offset === 1) return;
  setOffset((prevState) => prevState - 1);
};

// paginationの数字のonClickに渡す関数
// 前へ, 次へだけではなく、paginationの数字を押して直接offsetを変更できるようにします。
const changeOffset = (num: number) => {
  setOffset(num);
};
```

```ts
// offsetに従って表示すべきデータを取得する
// sliceを使用して表示すべきoffsetに合うデータの詰まった配列を返却します
// resultsの型は使用するデータによって異なるためジェネリクス型にしています。
function getShowData<T>(results: T[], offset: number, showCount: number): T[] {
  const firstArg = (offset - 1) * showCount;
  const secondArg = offset * showCount;
  return results.slice(firstArg, secondArg);
}

// pagination用のボタンの個数を提供
// paginationのボタン数はresultsの長さによって自動で取得できるようにしています
// JSXのbuttonをレンダリングする際にchangeOffsetに渡す引数を取り出したいためnumber[]を返却します
function getButtonCount<T>(results: T[], showCount: number): number[] {
  const necessaryButtonCount = Math.ceil(results.length / showCount);
  
  let resultCount = [];
  for (let i = 0; i < necessaryButtonCount; i++) {
    resultCount.push(i + 1);
  }
  return resultCount;
}
```

## JSX
tailwindcssのclass名を極力削除すると下記のようになります。
```js
<div>
  <p>結果</p>
  // コンテンツを表示
  {getShowData(results, offset, SHOW_COUNT).map((result) => {
    return <p key={result}>{result}</p>;
  })}
  
  <div className="flex">
    
    // prevButton 
    <button onClick={prevPage}>
      前へ
    </button>

    // paginationのbutton(数字)を必要個数を自動的に計算して表示
    {getButtonCount(results, SHOW_COUNT).map((count) => (
      <button key={count} onClick={() => changeOffset(count)}>
        {count}
      </button>
    ))}

    // nextButton
    <button onClick={nextPage}>
      次へ
    </button>
  </div>
  
</div>
```

## カスタムフック化
上記までで記事を投稿しようかとも思ったのですが、追加でカスタムフック化とリファクタリングもします。

ページネーション機能は複数ページで利用する可能性があります。
カスタムフックとしてロジックを切り出すことで、DRY(Don't Repeat Your Self：繰り返しを避けること)に実装することが可能です。
また今更感がありますが、先ほど紹介したコードにはリファクタリングできる要素がありますので同時に行いました。
主なリファクタリング内容は、関数の引数として渡していた値を直接stateから取得する方法に変えています。
また、Resultを汎用的に使えるようGenerics型を使用しました。

### usePagination.tsx (カスタムフック)

```tsx
import { useState } from "react";

// resutlsの型を使用時に決定できるようにGenerics型を使用
export const usePagination = <T,>() => {
  const [offset, setOffset] = useState(1);
  const [results, setResults] = useState<T[]>([]);
  // 1ページあたりの表示数はstateで管理する
  const [showCount, setShowCount] = useState(3);

  const nextPage = () => {
    const necessaryButtonCount = Math.ceil(results.length / showCount);
    if (necessaryButtonCount === offset) return;

    setOffset((prevState) => prevState + 1);
  };

  const prevPage = () => {
    if (offset === 1) return;
    setOffset((prevState) => prevState - 1);
  };

  const changeOffset = (num: number) => {
    setOffset(num);
  };

  // 変数として受け取っていた値を直接stateから受け取るように変更
  // Generics型はusePaginationで使用したGenericsをそのまま使う
  function getShowData(): T[] {
    const firstArg = (offset - 1) * showCount;
    const secondArg = offset * showCount;
    return results.slice(firstArg, secondArg);
  }

 // 変数として受け取っていた値を直接stateから受け取るように変更
  function getButtonCount(): number[] {
    const necessaryButtonCount = Math.ceil(results.length / showCount);

    let resultCount = [];
    for (let i = 0; i < necessaryButtonCount; i++) {
      resultCount.push(i + 1);
    }
    return resultCount;
  }

  return {
    setResults,
    setShowCount,
    prevPage,
    nextPage,
    changeOffset,
    getShowData,
    getButtonCount,
  };
};
```

### page.tsx (コンテンツを表示したいページ)

```tsx
"use client";
import { useEffect } from "react";
import { usePagination } from "./usePagination";

const items = [1, 2, 3, 4, 5, 6, 7, 8, 9];

export default function Home() {
  const {
    setResults,
    prevPage,
    nextPage,
    changeOffset,
    getShowData,
    getButtonCount,
  } = usePagination<number>(); // resultsの各要素の型(今回の場合はnumber)決定する

  // 本来はAPIでデータを取得してresultsに設定する
  useEffect(() => {
    setResults(items);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // 引数に値を渡す必要がなく見通しが改善される
  return (
    <div>
      <p>結果</p>
      {getShowData().map((result) => {
        return <p key={result}>{result}</p>;
      })}
      <div className="flex">
        <button
          className="border border-solid border-[#333]"
          onClick={prevPage}
        >
          前へ
        </button>
        {getButtonCount().map((count) => (
          <button
            key={count}
            className="rounded-full border border-solid border-black
          w-5 h-5 ml-1 flex justify-center items-center"
            onClick={() => changeOffset(count)}
          >
            {count}
          </button>
        ))}
        <button
          className="border border-solid border-[#333] ml-3"
          onClick={nextPage}
        >
          次へ
        </button>
      </div>
    </div>
  );
}
```

## 終わりに
今回紹介したページネーション機能は必要最低限の機能です。
これをベースに自身のプロジェクトに合わせて使用してください。
## 参考
https://typescriptbook.jp/reference/generics

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Math/ceil