
2025年6月24日
Next.jsを使用して、params
を使った動的ルーティングとページネーションを組み合わせた実装を紹介します。
記事のデータを用意して、それぞれのページにうまく表示させていきます。
また、トップページはページ番号で言うと1になり、ルーティングは必要ないので固定するイメージで行なっていきます。
トップページ(1ページ目)
こちらが今回表示したいデータです。
// 仮データ
export const mockPosts = [
{ id: 1, title: 'Getting Started with Next.js' },
{ id: 2, title: 'Understanding App Router in Next.js 15' },
{ id: 3, title: 'Using Prisma with PostgreSQL' },
{ id: 4, title: 'Building a Blog with Markdown Support' },
{ id: 5, title: 'Adding Image Upload to Your Forms' },
{ id: 6, title: 'Implementing Tag Filtering' },
{ id: 7, title: 'Responsive Design with Tailwind CSS' },
{ id: 8, title: 'Authentication with next-auth' },
{ id: 9, title: 'Previewing Markdown in Real Time' },
{ id: 10, title: 'Deploying to Vercel' },
];
トップページから実装していきます。
// 1ページあたりのアイテムの数
export const POSTS_PER_PAGE = 5;
// 合計何ページ必要か計算
export const totalPages = Math.ceil(mockPosts.length / POSTS_PER_PAGE);
1ページあたり5つデータを表示するとしましょう。また、必要なページ数は全部で2ページになります。(Math.ceil()
を使い、中の数字を切り上げます。今回は綺麗に割り切れてます。)
export default function Page() {
// 1ページあたりに必要なアイテムの数だけsliceする
const paginatedPosts = mockPosts.slice(0, POSTS_PER_PAGE);
return (
<div className="mx-auto">
<div className="grid grid-cols-5 gap-4">
{paginatedPosts.map(post => (
<div key={post.id}>
{post.title}
</div>
))}
</div>
<div className="flex gap-4">
{Array.from({ length: totalPages }).map((_, i) =>
i === 0 ? (
<span key={i} className="bg-blue-300 px-3 py-1 rounded">
{i + 1}
</span>
) : (
<Link
key={i}
href={`/post/${i + 1}`}
className="bg-blue-300 px-3 py-1 rounded"
>
{i + 1}
</Link>
)
)}
</div>
</div>
);
}
1ページ目なので、slice()
で0番目からスタートすれば必要な数だけデータを取れます。そして実際に表示されるデータは、mockPosts[0]
〜mockPosts[4]
の5つになります。
次にルーティングの部分です。
ページ番号を表示するためtotalPages
を用いて、ページ数の合計を長さとする配列をArray.from()
で作ります。今回は全部で2ページなので、長さが2
の配列です。<Link>
コンポーネントの/post/${i + 1}
で、ページ番号をパラメータとしてparamsを使えるようにルーティングします。
また、i === 0
の時、つまり1ページ目の時はルーティングをさせたくないので(トップページのままでいいから)span
要素を使って表示しています。
トップページの実装はとりあえずこのような感じです。
動的ルーティングページ
次に実際にこのページネーションで遷移されるページを実装していきます。/post/[page]/page.tsx
にコードを書いていきます。先に書いた/post/${i + 1}
で表示されるページになります。
const Page = async ({ params }: Params) => {
const { page: currentPage } = await params;
// 1ページあたりに必要なデータだけ取得
const paginatedPosts = mockPosts.slice(
POSTS_PER_PAGE * (currentPage - 1),
POSTS_PER_PAGE * currentPage
);
return (
<div>
<div>
{paginatedPosts.map(post => (
<div key={post.id}>
<p>{post.title}</p>
</div>
))}
</div>
<div>
{Array.from({ length: totalPages }).map((_, i) => {
const isActive = currentPage - 1 === i;
if (i === 0) {
return (
<Link key={i} href="/">
{i + 1}
</Link>
);
}
return isActive ? (
<span key={i}>
{i + 1}
</span>
) : (
<Link
key={i}
href={`/post/${i + 1}`}
>
{i + 1}
</Link>
);
})}
</div>
</div>
);
};
まずparamsから現在のページ番号(currentPage)
を取得します。
そして1ページあたりに必要なデータだけslice()
で取ります。
const paginatedPosts = mockPosts.slice(
POSTS_PER_PAGE * (currentPage - 1),
POSTS_PER_PAGE * currentPage
);
この式は少し複雑ですが、一般化された形になっています:
- 1ページ目(currentPage = 1): slice(0, 5) → インデックス0〜4の記事
- 2ページ目(currentPage = 2): slice(5, 10) → インデックス5〜9の記事
1ページあたりに表示したい記事数(POSTS_PER_PAGE)や、現在のページ番号が変わったとしても使える汎用的な式です。
次に先ほどと同じように配列を作ってページネーションを実装していきます。
const isActive = currentPage - 1 === i;
ページネーションの数字が現在のページ番号に等しい場合、ルーティングを行いたくないので後にこの式で分岐してspan
要素を使います。
if (i === 0) {
return (
<Link key={i} href="/">
{i + 1}
</Link>
);
}
i === 0
の時つまりページ番号が1の時は、トップページに遷移させたいのでこのように条件分岐します。
次に2つ目の条件分岐です。
現在表示されているページ番号には、ルーティングをさせたくないのでこのように分岐させます。
return isActive ? (
<span key={i}>{i + 1}</span>
) : (
<Link key={i} href={`/post/${i + 1}`}>
{i + 1}
</Link>
);
2ページ目はこのように表示されてます。
まとめ
以上の方法で基本的なページネーションを動的ルーティングと組み合わせて実装できます。 POSTS_PER_PAGE
やデータの数を増やしたりするともっとわかりやすいと思います。
ぜひ皆さんも試してみて下さい。