家計簿アプリを作る #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 で内部ボタンに適用 |