JavaScript | 配列・オブジェクト:実務パターン – ページング用データ加工

JavaScript JavaScript
スポンサーリンク

ページング用データ加工とは何か

ページングは「長い配列をページ単位に切り分けて、必要な部分だけ表示・送信する」処理です。ここが重要です:順序(ソート)→フィルタ→ページ分割の“処理順”を守り、メタ情報(総件数、総ページ、現在ページの開始・終了インデックス)を一緒に返すと、UI・APIの両方で扱いやすくなります。


基本のページング(ページ番号+件数)

スライスで“取り出す範囲”を決める

function paginate(list, page = 1, perPage = 20) {
  const total = list.length;
  const totalPages = Math.max(1, Math.ceil(total / perPage));
  const current = Math.min(Math.max(1, page), totalPages); // 範囲に収める

  const start = (current - 1) * perPage;
  const end = start + perPage; // slice は end 未満
  const rows = list.slice(start, end);

  return { rows, page: current, perPage, total, totalPages, start, end: Math.min(end, total) };
}

// 使い方
const data = Array.from({ length: 53 }, (_, i) => i + 1);
paginate(data, 3, 10); // 21..30 が rows
JavaScript

ここが重要です:page の境界を必ず正規化(1〜totalPages)し、slice の範囲を一貫して計算します。end は“未満”であることに注意。


フィルタ・並び替えとページングの順序

まず整形(フィルタ・ソート)してからページング

function applyFiltersAndPaging(list, { q, sort, page, perPage }) {
  // フィルタ(例:名前に q が含まれる)
  const normalize = s => s.trim().toLowerCase();
  const filtered = q
    ? list.filter(x => normalize(x.name).includes(normalize(q)))
    : list;

  // 並び替え(例:価格昇順/降順)
  const sorted = sort === "priceDesc"
    ? filtered.toSorted((a, b) => b.price - a.price)
    : filtered.toSorted((a, b) => a.price - b.price);

  // ページング
  return paginate(sorted, page, perPage);
}
JavaScript

ここが重要です:フィルタやソートの後にページングしないと、ページ境界がズレて“見えるはずのデータが消える”問題が起こります。常に「整形→ページング」の順。


ページングメタの設計(UIが使いやすい補助情報)

次・前ページの可否、ページボタンなど

function withNav(meta) {
  const { page, totalPages } = meta;
  return {
    ...meta,
    hasPrev: page > 1,
    hasNext: page < totalPages,
    prevPage: page > 1 ? page - 1 : null,
    nextPage: page < totalPages ? page + 1 : null
  };
}

// 例
const result = withNav(paginate(data, 1, 10));
JavaScript

ここが重要です:UIが“条件分岐なし”でボタン制御できるよう、hasPrev/hasNext などのフラグを返すと使い勝手が上がります。


サーバー/クライアントでの違い(offset と cursor)

オフセット型(page/perPage)—シンプルで理解しやすい

// クエリ例:?page=3&perPage=20
const start = (page - 1) * perPage;
const rows = list.slice(start, start + perPage);
JavaScript

ここが重要です:並びが安定している(ソート固定)の前提で有効。大量データでは“後ろのページほど重い”課題があり、サーバー側ではインデックス最適化が必要です。

カーソル型(cursor/limit)—大量・変更に強い

// 疑似例(client側):前回の最後のIDをカーソルに
function pageByCursor(list, cursorId, limit = 20) {
  const startIndex = cursorId ? list.findIndex(x => x.id === cursorId) + 1 : 0;
  const rows = list.slice(startIndex, startIndex + limit);
  const nextCursor = rows.at(-1)?.id ?? null;
  return { rows, nextCursor, limit };
}
JavaScript

ここが重要です:データの追加・削除があっても重複・取りこぼしが起きにくいのが利点。API実務ではカーソル+安定ソート(IDや作成日時)をセットにします。


エッジケースと安全策(重要ポイントの深掘り)

空・最終ページ・perPage 変更の取り扱い

  • 空配列なら totalPages=1 として page=1 を返すとUIが扱いやすい。
  • perPage を動的に変えたら、page を再正規化(大きすぎるページは最終ページへ)する。
function safePaginate(list, page, perPage) {
  const per = Math.max(1, perPage | 0);
  const meta = paginate(list, page, per);
  return withNav(meta);
}
JavaScript

欠損や型揺れの防御

ページ/件数は整数へ正規化(| 0parseInt)、負数や0は最小値へ丸める。文字列のソートは localeCompare、数値は a - b に統一。

フィルタ・ソートの重さを軽減(前計算)

頻繁にページングするなら、比較に使うキー(normalize済み文字列、new Date(...).getTime()など)を前計算して持たせ、スライス前に高速に判定できるようにする。

const prepared = list.map(x => ({ x, key: x.name.toLowerCase() }));
const filtered = prepared.filter(p => p.key.includes("book")).map(p => p.x);
JavaScript

実務で便利な拡張(ヘッダ・フッタ・合計・ページ行番号)

ページ内の“連番”や“行番号”を付ける

function paginateWithIndex(list, page, perPage) {
  const meta = paginate(list, page, perPage);
  const rows = meta.rows.map((x, i) => ({ ...x, rowNo: meta.start + i + 1 }));
  return { ...meta, rows };
}
JavaScript

ここが重要です:ページ内に通し番号があると、UIで参照が楽になり、CSV出力などにも向きます。

ヘッダ・フッタ用の要約(合計など)

function summarizePage({ rows }) {
  const sum = rows.reduce((a, x) => a + (x.price ?? 0), 0);
  return { sum, count: rows.length };
}
JavaScript

ここが重要です:ページ内集計は“切り出して”返すと、UIが直で使えます。総合計は全体から別途計算します。


すぐ使えるレシピ(定番ユーティリティ)

ページング(メタ付き・安全版)

const paginate = (list, page = 1, perPage = 20) => {
  const per = Math.max(1, perPage | 0);
  const total = list.length;
  const totalPages = Math.max(1, Math.ceil(total / per));
  const current = Math.min(Math.max(1, page | 0), totalPages);
  const start = (current - 1) * per;
  const end = Math.min(start + per, total);
  return {
    rows: list.slice(start, end),
    page: current,
    perPage: per,
    total,
    totalPages,
    start,
    end,
    hasPrev: current > 1,
    hasNext: current < totalPages,
    prevPage: current > 1 ? current - 1 : null,
    nextPage: current < totalPages ? current + 1 : null
  };
};
JavaScript

整形→ページング(フィルタ+ソート+ページ)

const normalize = s => s.trim().toLowerCase();

function querySortPage(list, { q, sort = "nameAsc", page = 1, perPage = 20 }) {
  const filtered = q ? list.filter(x => normalize(x.name).includes(normalize(q))) : list;

  const cmp =
    sort === "priceAsc"  ? ((a, b) => a.price - b.price) :
    sort === "priceDesc" ? ((a, b) => b.price - a.price) :
    ((a, b) => a.name.localeCompare(b.name, "ja", { numeric: true }));

  const sorted = filtered.toSorted(cmp);
  return paginate(sorted, page, perPage);
}
JavaScript

カーソル型(次ページ用の印を返す)

function cursorPage(list, cursorId = null, limit = 20) {
  const i = cursorId == null ? -1 : list.findIndex(x => x.id === cursorId);
  const start = Math.max(0, i + 1);
  const rows = list.slice(start, start + limit);
  return { rows, nextCursor: rows.at(-1)?.id ?? null, limit };
}
JavaScript

まとめ

ページング加工の核心は「整形(フィルタ・ソート)→ページ分割→メタを返す」という流れを徹底することです。page/perPage を正規化し、slice の範囲を一貫して計算、UI向けに hasPrev/hasNext や行番号などの補助情報を付ける。大量・変動データにはカーソル型が有効で、重い処理は前計算で軽くする。これを押さえれば、初心者でも読みやすく、拡張しやすい、実務に耐えるページング処理が書けます。

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