家計簿アプリを作る #22:収支一覧のページネーション

家計簿アプリ作成シリーズの第22回です。データが増えたときに全件表示では見づらくなるため、ページネーションを実装します。

実装方針

  • ?page=1 のクエリパラメータでページ番号を管理
  • allTransactions(全件取得済み)から filter + slice でページネーション
  • 集計(収入・支出・残高)はページネーション前の月全件から計算
  • UIは flowbite-svelte の PaginationNav コンポーネントを使用

Step 1:+page.server.ts の実装

const PAGE_SIZE = 5;
const page = Number(url.searchParams.get('page') ?? '1');
const offset = (page - 1) * PAGE_SIZE;

// 選択月の全件(allTransactionsから絞り込み)
const monthTransactions = allTransactions.filter(t =>
  t.date.startsWith(selectedMonth ?? '')
);
const totalCount = monthTransactions.length;
const totalPages = Math.ceil(totalCount / PAGE_SIZE);

// 表示用:sliceでページネーション
const selectedTransactions = monthTransactions.slice(offset, offset + PAGE_SIZE);

// 集計はmonthTransactions(全件)から計算
const income = monthTransactions
  .filter(t => t.type === 'income')
  .reduce((sum, t) => sum + t.amount, 0);
const expense = monthTransactions
  .filter(t => t.type === 'expense')
  .reduce((sum, t) => sum + t.amount, 0);

return {
  transactions: selectedTransactions,
  totalIncome: income,
  totalExpense: expense,
  balance: income - expense,
  totalPages,
  // ...
};

集計をページネーション後の selectedTransactions から計算すると、表示中のページ分しか集計されないため注意が必要です。必ず月全件の monthTransactions から計算します。

Step 2:+page.svelte の実装

<script lang="ts">
  import { goto } from "$app/navigation";
  import { PaginationNav } from "flowbite-svelte";

  let { data } = $props();

  let currentPage = $state(1);

  function handlePageChange(page: number) {
    currentPage = page;
    goto(`/?month=${data.selectedMonth}&page=${currentPage}`);
  }
</script>

<div class="flex justify-center mt-4">
  <PaginationNav
    {currentPage}
    totalPages={data.totalPages}
    onPageChange={handlePageChange}
    class="border-gray-200 [&_button]:border-gray-300"
  />
</div>

ハマりポイント①:totalPages を変数に入れるとワーニングが出る

// ❌ ワーニングが出る
let totalPages = $state(data.totalPages);
<PaginationNav totalPages={totalPages} ... />

// ✅ 直接渡す
<PaginationNav totalPages={data.totalPages} ... />

data を変数に代入すると「初回の値しかキャプチャされない」というワーニングが出ます。data.totalPages を直接渡すことで、月切り替え時にも最新の値が反映されます。

ハマりポイント②:URL パラメータの区切り文字

❌ http://localhost:5174/?month=2026-05?page=2
✅ http://localhost:5174/?month=2026-05&page=2

複数のクエリパラメータは & で区切ります。? はクエリ文字列の開始を示す記号なので2つ目以降には使えません。

ハマりポイント③:月切り替え時に page をリセットする

年月セレクトで月を切り替えるとき、page=1 をセットしないと前のページ番号が残ります。

<Select
  onchange={(e) => goto(`/?month=${e.currentTarget.value}&page=1`)}
>

PaginationNav のスタイル調整

PaginationNav のボーダー色は class で上書きできます。

<PaginationNav class="border-gray-200 [&_button]:border-gray-300" />

[&_button] で内部の全ボタンにスタイルを適用します。活性中のボタンはもともと border-gray-300 が付いているため、非活性を border-gray-200 に下げることで自然に差がつきます。

まとめ

ポイント 内容
集計の計算対象 ページネーション前の月全件から計算する
totalPages の渡し方 data.totalPages を直接渡す(変数に入れない)
URL パラメータの区切り 2つ目以降は & を使う
月切り替え時の page リセット &page=1 を付けて遷移する
ボーダー色の上書き [&_button]:border-gray-300 で内部ボタンに適用
← トップページに戻る