概要
Next.js App Router の searchParams と Link コンポーネントを使い、URL クエリパラメータ(?page=N)と同期したページネーションを実装する。Server Component でデータを取得するため、JavaScript なしでもページ遷移が機能する。tanstack-query-pagination との違いはサーバー側レンダリング vs クライアント側フェッチ。
インストール
# 追加インストールは不要
実装
ページネーションコンポーネント
// components/Pagination.tsx
import Link from "next/link";
type Props = {
currentPage: number;
totalPages: number;
basePath: string;
};
export function Pagination({ currentPage, totalPages, basePath }: Props) {
return (
<nav className="flex items-center gap-2">
{currentPage > 1 && (
<Link
href={`${basePath}?page=${currentPage - 1}`}
className="rounded border px-3 py-1 text-sm hover:bg-gray-100"
>
前へ
</Link>
)}
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<Link
key={page}
href={`${basePath}?page=${page}`}
className={`rounded border px-3 py-1 text-sm ${
page === currentPage
? "bg-blue-600 text-white"
: "hover:bg-gray-100"
}`}
>
{page}
</Link>
))}
{currentPage < totalPages && (
<Link
href={`${basePath}?page=${currentPage + 1}`}
className="rounded border px-3 py-1 text-sm hover:bg-gray-100"
>
次へ
</Link>
)}
</nav>
);
}
データ取得ユーティリティ
// lib/getPaginatedItems.ts
const ITEMS_PER_PAGE = 10;
type PaginatedResult<T> = {
items: T[];
totalPages: number;
currentPage: number;
};
export function getPaginatedItems<T>(
allItems: T[],
page: number
): PaginatedResult<T> {
const totalPages = Math.ceil(allItems.length / ITEMS_PER_PAGE);
const currentPage = Math.min(Math.max(1, page), totalPages);
const start = (currentPage - 1) * ITEMS_PER_PAGE;
const items = allItems.slice(start, start + ITEMS_PER_PAGE);
return { items, totalPages, currentPage };
}
ページコンポーネント(Server Component)
// app/posts/page.tsx
import { Pagination } from "@/components/Pagination";
import { getPaginatedItems } from "@/lib/getPaginatedItems";
// ダミーデータ(実際は DB やAPI から取得)
const allPosts = Array.from({ length: 47 }, (_, i) => ({
id: i + 1,
title: `記事タイトル ${i + 1}`,
}));
type Props = {
searchParams: Promise<{ page?: string }>;
};
export default async function PostsPage({ searchParams }: Props) {
const { page } = await searchParams;
const currentPage = Number(page) || 1;
const { items, totalPages } = getPaginatedItems(allPosts, currentPage);
return (
<main className="mx-auto max-w-2xl p-8">
<h1 className="mb-6 text-2xl font-bold">記事一覧</h1>
<ul className="mb-8 space-y-2">
{items.map((post) => (
<li key={post.id} className="rounded border p-3 text-gray-700">
{post.title}
</li>
))}
</ul>
<Pagination
currentPage={currentPage}
totalPages={totalPages}
basePath="/posts"
/>
</main>
);
}
ポイント
searchParamsは Server Component で直接受け取れる。Next.js 15 ではPromise<{...}>型になっているためawaitが必要Linkコンポーネントで?page=Nのクエリ付き URL へ遷移するだけで、URL とページ状態が自動的に同期する- クライアントコンポーネント不要のため、JavaScript なしでも機能するシンプルな構成
Math.min(Math.max(1, page), totalPages)で範囲外のページ番号を安全に処理するtanstack-query-paginationとの使い分け: SEO が必要なページや JS なし環境では URL 同期 Server Component が適切