JavaScript Tips | 配列ユーティリティ:ページング

JavaScript JavaScript
スポンサーリンク

何をしたいユーティリティか:「配列のページング」

ここでの「ページング」は、たくさんのデータを「ページ番号」と「1ページあたり件数」に分割して扱う処理のことです。
Web の一覧画面でよく見る「1〜10件」「11〜20件」「次へ」「前へ」のあれです。

業務では、例えば次のような場面で使います。
検索結果を 50 件ずつ表示したい。
ログ一覧を 100 件ずつに区切って表示したい。
API から受け取った配列を、フロント側でページングしたい。

ここでは、「ページングの考え方」→「基本的なユーティリティ」→「実務で便利な情報も返すユーティリティ」という流れでかみ砕いていきます。


ページングの基本的な考え方

「何ページ目の、どの範囲を切り出すか」

配列のページングは、結局のところ「配列の一部を slice で切り出す」だけです。
大事なのは、「どこからどこまでを切り出すか」をきちんと計算することです。

ページ番号を page(1 始まり)、1 ページあたりの件数を pageSize とすると、
そのページの開始インデックスと終了インデックスは次のように計算できます。

開始インデックス(0 始まり)
start = (page - 1) * pageSize

終了インデックス(slice の第2引数用)
end = start + pageSize

例えば、page = 2pageSize = 10 のとき、
start = 10end = 20 なので、「インデックス 10〜19」の要素が 2 ページ目になります。

この「start と end を計算して slice する」というパターンさえ理解すれば、ページングは怖くありません。


一番基本:配列をページングする関数

シンプルなページングユーティリティ

まずは、「指定したページの要素だけを返す」シンプルな関数です。

function paginate(array, page, pageSize) {
  if (!Array.isArray(array)) {
    return [];
  }

  const safePageSize = pageSize > 0 ? pageSize : 1;
  const safePage = page > 0 ? page : 1;

  const start = (safePage - 1) * safePageSize;
  const end = start + safePageSize;

  return array.slice(start, end);
}
JavaScript

ここでやっていることをかみ砕きます。

まず、pageSizepage が 0 以下だった場合に備えて、「最低 1」に補正しています。
ページ番号が 0 やマイナスだと計算が破綻するので、1 ページ目として扱うようにしています。

次に、先ほど説明した通り startend を計算し、slice でその範囲を切り出しています。
slice は、配列の範囲外を指定してもエラーにならず、「存在する部分だけ」を返してくれるので、ページングと相性が良いです。

実際の動き

const data = Array.from({ length: 25 }, (_, i) => i + 1);
// [1, 2, 3, ..., 25]

paginate(data, 1, 10);
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

paginate(data, 2, 10);
// [11, 12, 13, 14, 15, 16, 17, 18, 19, 20]

paginate(data, 3, 10);
// [21, 22, 23, 24, 25]

paginate(data, 4, 10);
// []  (4ページ目はもうデータがない)
JavaScript

このように、「ページ番号とページサイズから、欲しい部分だけを切り出す」のがページングの基本です。


実務で便利な情報も返すページングユーティリティ

「ページの中身だけ」だと足りないことが多い

実務では、「ページの中身」だけでなく、次のような情報も欲しくなります。

全体の件数(totalCount)
全体のページ数(totalPages)
今のページ番号(page)
前のページがあるか(hasPrev)
次のページがあるか(hasNext)

これらを毎回呼び出し側で計算するのは面倒なので、ユーティリティ側でまとめて返すようにします。

情報付きページングユーティリティ

function paginateWithInfo(array, page, pageSize) {
  const items = Array.isArray(array) ? array : [];

  const safePageSize = pageSize > 0 ? pageSize : 1;
  const totalCount = items.length;
  const totalPages = Math.max(1, Math.ceil(totalCount / safePageSize));

  let safePage = page > 0 ? page : 1;
  if (safePage > totalPages) {
    safePage = totalPages;
  }

  const start = (safePage - 1) * safePageSize;
  const end = start + safePageSize;
  const pageItems = items.slice(start, end);

  return {
    items: pageItems,
    page: safePage,
    pageSize: safePageSize,
    totalCount,
    totalPages,
    hasPrev: safePage > 1,
    hasNext: safePage < totalPages,
  };
}
JavaScript

ここでの重要ポイントを深掘りします。

まず、totalCount は配列の長さです。
totalPages は「全体を pageSize で割って切り上げた値」です。
例えば 25 件を 10 件ずつに分けるなら、Math.ceil(25 / 10) = 3 で 3 ページになります。

ページ番号 safePage は、1 未満なら 1、totalPages より大きければ totalPages に丸めています。
これにより、「存在しないページ番号」を指定されても、最後のページに丸めてくれます。

最後に、hasPrevhasNext を計算しています。
1 ページ目なら hasPrev は false、最終ページなら hasNext は false になります。

実際の動き

const data = Array.from({ length: 25 }, (_, i) => i + 1);

paginateWithInfo(data, 1, 10);
/*
{
  items: [1..10],
  page: 1,
  pageSize: 10,
  totalCount: 25,
  totalPages: 3,
  hasPrev: false,
  hasNext: true,
}
*/

paginateWithInfo(data, 3, 10);
/*
{
  items: [21, 22, 23, 24, 25],
  page: 3,
  pageSize: 10,
  totalCount: 25,
  totalPages: 3,
  hasPrev: true,
  hasNext: false,
}
*/

paginateWithInfo(data, 999, 10);
/*
page は 3 に丸められ、最後のページが返る
*/
JavaScript

このように、「ページングに必要な情報一式」をまとめて返すと、画面側の実装がかなり楽になります。


ページングとソート・フィルタの関係

「ソート・フィルタ → ページング」の順番が基本

実務では、ページングだけでなく、ソートやフィルタも一緒に使うことが多いです。

例えば、
「価格の安い順に並べてから、10 件ずつページングする」
「カテゴリで絞り込んでから、ページングする」

このときの基本的な順番は、

ソートする
フィルタする
その結果に対してページングする

です。

ページングを先にやってしまうと、「全体の中での順番」ではなく「元配列の順番」で切り出されてしまうので、
ユーザーの期待する結果とズレてしまいます。

実際のコード例

const sorted = sortBy(products, (p) => p.price, "asc");
const filtered = sorted.filter((p) => p.category === "Food");
const pageResult = paginateWithInfo(filtered, 1, 10);
JavaScript

このように、「ソート・フィルタのあとにページング」という流れをユーティリティレベルで固定しておくと、
バグを減らしやすくなります。


実務で意識してほしい設計のポイント

ページ番号は「1 始まり」で扱う

配列のインデックスは 0 始まりですが、ページ番号は 1 始まりで扱うのが自然です。
ユーザーに「0 ページ目」と表示することはまずないからです。

内部では 0 始まりのインデックスを使いつつ、
外から受け取る page は 1 始まり、と決めておくと混乱が減ります。

「範囲外のページ番号」をどう扱うかを決める

page = 0page = -1page = 9999 のような値が来ることは普通にあります。
ここをどう扱うかを、ユーティリティ側で決めておくのが大事です。

今回の paginateWithInfo では、

1 未満 → 1 に丸める
最大ページより大きい → 最大ページに丸める

という仕様にしています。
これにより、「とりあえず有効なページを返す」挙動になります。

もし「範囲外なら空配列を返したい」など別の要件があるなら、
そのポリシーをユーティリティに閉じ込めておくと、呼び出し側がシンプルになります。

「全件数」と「ページ数」を一緒に返す意味

ページングでは、「今のページの中身」だけでなく、
「全体で何件あるのか」「何ページあるのか」が UI にとって非常に重要です。

例えば、
「全 123 件中 21〜30 件を表示」
「3 / 13 ページ」

といった表示をするには、totalCounttotalPages が必須です。

これを毎回呼び出し側で計算するのではなく、
ページングユーティリティが一緒に返してくれるようにしておくと、
どの画面でも同じロジックを再利用できるようになります。


少し手を動かして感覚をつかむ

コンソールで、次のようなコードを実際に打ってみてください。

const data = Array.from({ length: 53 }, (_, i) => i + 1);

paginate(data, 1, 10);
paginate(data, 2, 10);
paginate(data, 6, 10);

paginateWithInfo(data, 1, 10);
paginateWithInfo(data, 6, 10);
paginateWithInfo(data, 999, 10);
JavaScript

「どのページにどの値が入っているか」「最終ページがどう扱われているか」「範囲外のページ番号がどう丸められているか」を、自分の目で確認してみてください。

そのうえで、自分のプロジェクトに

export function paginate(...) { ... }
export function paginateWithInfo(...) { ... }
JavaScript

のような関数を置き、

「配列をページングしたくなったら、必ずこの“ページングユーティリティ”を通す」

というルールを作ってみてください。
そうすると、あなたの一覧画面や API レスポンスは、「場当たり的な slice」から、「意図と一貫性を備えた業務レベルのページング実装」に一段ステップアップしていきます。

タイトルとURLをコピーしました