レビュー済み·難易度: 中級·更新: 2026-04-18

Next.js App Router で URL 同期ページネーションを実装する

searchParams を受け取る Server Component と Link コンポーネントを使い、URL クエリ(?page=N)と同期したページネーションを実装する例。

nextjspaginationrouting

対応バージョン

nextjs 15react 19

前提環境

Next.js App Router の Server Component と searchParams の基本を理解していること

概要

Next.js App Router の searchParamsLink コンポーネントを使い、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 が適切

注意点

クライアントコンポーネントではなく Server Component で実装するため、JavaScript なしでもページ遷移が機能する。tanstack-query-pagination との違いはサーバー側レンダリング vs クライアント側フェッチ。

関連サンプル