栽培管理アプリを作ってます⑭-1:ページネーションを実装した話【Next.js × Supabase】

目次

データが「消えた」?と思ったら…

ある日、作業履歴を確認していると、古いデータが表示されなくなっていました。

でも、Supabaseのデータベースを確認すると、データはちゃんと残っています。

「データはあるのに、画面に出ない」

そういえば、開発初期に仮で入れた limit をすっかり忘れていただけでした。

.order("log_date", { ascending: false })
.order("created_at", { ascending: false })
.limit(100)

新しい100件だけ取得していた、という状態です。

もう、ログが100件を超えたのか・・・と思っていたのですが、limit(100) で取得件数を絞ったままにすると過去の履歴が見えない。制限を外して全件表示にしたとしても、今度はひたすら下にスクロールしないといけない。それはそれで使いにくい。


と、いうことでlimitを外してフロントでページネーション

limit を外すことにしましたが、次に考えたのはどの単位でページネーションするかです。

このアプリでは画面が日付ごとにグループ表示されているため、ログ件数で区切ると「同じ日付が2ページにまたがる」という問題が起きます。

例えば、下記のような作業があったとき

ユーザーが見ている単位は「1ログ」ではなく「1日分のまとまり」です。

もしDB側で100ログずつ区切ると、こんな問題が起きます。

例:各日のログ数がこうだった場合

日付ログ数累計
2/1610件10件
2/178件18件
2/1885件103件
2/16を例にすると、画面上は2件に見えても、DBには複数ハウスなどのレコード(10件のログ)が存在する

100件目の境目で、2月18日の途中でページが切れます。同じ日付が1ページ目と2ページ目に分断されてしまいます。

そこで、フロント側で日付グループ単位のページネーションを実装することにしました。

今回の仕様

  • 1ページ = 20日分(日付グループ単位)の履歴を表示
  • 20日分を超えると「次へ」ボタンを表示

実装コード

まずページ管理のstateとページあたりの件数を定義します。

const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 20;

次に、useMemo を使って現在のページに表示するデータを切り出します。

const paginatedWorksByGroup = useMemo(() => {
  const startIndex = (currentPage - 1) * itemsPerPage;
  const endIndex = startIndex + itemsPerPage;
  return worksByGroup.slice(startIndex, endIndex);
}, [worksByGroup, currentPage]);

総ページ数の計算はこうなります。

const totalPages = Math.ceil(worksByGroup.length / itemsPerPage);

これで21日分のデータがあれば、自動的に「1 / 2」「次へ」ボタンが表示されます。


フィルタ・カレンダーとの整合性

ページネーションを追加したとき、既存のフィルタ・カレンダー機能への影響が気になりました。

結果として:

  • フィルタ:正常に動作(ページネーションと独立して機能)
  • カレンダー:全期間の作業日を表示(ページネーションの影響なし)

ページネーションは「一覧の表示件数」にのみ影響し、カレンダーは「全ログ」を元に表示しています。これは結果的に自然な挙動でした。

今の設計と、将来的な改善案

今の設計(現状)

現在のログ数はそれほど多くないため、こういう方針にしています。

  1. Supabaseから全ログを一括取得
  2. フロントで集計・グループ化
  3. フロントでページネーション

シンプルで問題ありません。

将来的にログが増えた場合

ログが数千件になったとき、全件取得は重くなります。そのときは以下のように分離する予定です。

一覧表示

// ページ単位でサーバーから取得
.from("cult_logs")
.select("*")
.range(startIndex, endIndex)

カレンダー

// 月単位で日付だけ取得
.from("cult_logs")
.select("log_date")
.gte("log_date", "2026-02-01")
.lte("log_date", "2026-02-28")

取得データが小さくなり、パフォーマンスが安定し、責務も分離できます。今は不要ですが、設計として頭に入れておくと後で困りません。

現在と今後ののアプローチの違いをまとめるとこうなります。

比較DBページネーション(件数単位)今回の実装(日付単位)
どこで処理するかサーバー(Supabase)ブラウザ(フロント)
ページの区切りログ100件ごと日付20日分ごと
日付をまたぐ問題発生する発生しない
データ量が増えたとき軽い重くなる可能性あり

一言でいうと、「誰が・何を基準に切るか」が違います。


まとめ

今回やったこと:

  • limit 制限を外してページネーションを実装
  • 1ページ20日分の表示に変更
  • フィルタ・カレンダーとの整合性を確認
目次