【11-2】Next.js app routerのチュートリアルやってみる(URL パラメータを利用したページネーション)

# はじめに

Next.js app routerのチュートリアルの第11章-2のアウトプットをします。

https://nextjs.org/learn/dashboard-app/adding-search-and-pagination

<details><summary>前の記事</summary>

【01】Next.js app routerのチュートリアルやってみる

https://qiita.com/naoyuki2/items/af58da3d20cbc790e767

【02】Next.js app routerのチュートリアルやってみる

https://qiita.com/naoyuki2/items/edf450b3ee135e83d1e8

【03】Next.js app routerのチュートリアルやってみる
https://qiita.com/naoyuki2/items/612221eac233aa9cbb74

【04】Next.js app routerのチュートリアルやってみる

https://qiita.com/naoyuki2/items/62f9beccbfe36eaf7f90

【05】Next.js app routerのチュートリアルやってみる

https://qiita.com/naoyuki2/items/8b71b1d1df7c9435a9c9

【06】Next.js app routerのチュートリアルやってみる

https://qiita.com/naoyuki2/items/58130c3cfbaf8a573de2

【07】Next.js app routerのチュートリアルやってみる

https://qiita.com/naoyuki2/items/2c2da0f8071e60454679

【08】Next.js app routerのチュートリアルやってみる

https://qiita.com/naoyuki2/items/45f45fcb9cc14506f79f

【09】Next.js app routerのチュートリアルやってみる(loading.tsxとSuspenseでストリーミング)

https://qiita.com/naoyuki2/items/717694288ec6017a3af2

【10】Next.js app routerのチュートリアルやってみる(部分的な事前レンダリング)

https://qiita.com/naoyuki2/items/8062f755b0679fe925b1

【11-1】Next.js app routerのチュートリアルやってみる(URLパラメーターを利用した検索機能)

https://qiita.com/naoyuki2/items/2be9503ac80fc4a1fa6a

</details>

# 第11-2章 ページネーション

この章では以下を学びました。

- `ページネーション`の実装

第11章の後編です。

# ページネーションの追加

前回検索機能を実装できましたが、現在一度に６つまでしかデータを表示できません。

`fetchFilterdInvoices`関数は一度に６つしかデータを取ってこないからです。

```diff_tsx:/app/dashboard/invoices/table.tsx
const invoices = await fetchFilteredInvoices(query, currentPage);
```

```diff_tsx:/app/lib/data.ts
const ITEMS_PER_PAGE = 6
export async function fetchFilteredInvoices(
    query: string,
    currentPage: number,
) {
    noStore()
    const offset = (currentPage - 1) * ITEMS_PER_PAGE

    try {
        const invoices = await sql<InvoicesTable>`
      SELECT
       // ....
      LIMIT ${ITEMS_PER_PAGE} OFFSET ${offset}
    `

        return invoices.rows
    } catch (error) {
        console.error('Database Error:', error)
        throw new Error('Failed to fetch invoices.')
    }
}
```

そのため、ページネーションを実装して、すべてのデータを見れるようにしましょう。

前回の検索機能と同じように、URLパラメーターを利用してページネーションを実現します。

### 合計ページ数を取得

合計ページ数が分からないことには`ページネーション`は実装できません。

`fetchInvoicesPages`という関数を`import`します。

そして、この関数に`query`(検索キーワード)を渡すことで、合計ページ数が取得できます。

```diff_tsx:/app/dashboard/invoices/page.tsx
// ...
+import { fetchInvoicesPages } from '@/app/lib/data';
 
export default async function Page({
  searchParams,
}: {
  searchParams?: {
    query?: string,
    page?: string,
  },
}) {
  const query = searchParams?.query || '';
  const currentPage = Number(searchParams?.page) || 1;
+  const totalPages = await fetchInvoicesPages(query);
 
  return (
    // ...
  );
}
```

そして、合計ページ数`totalPages`を`<Pagination />`コンポーネントに渡しましょう。

```diff_tsx:/app/dashboard/invoices/page.tsx
<Pagination totalPages={totalPages} />
```

`<Pagination />`コンポーネントに移動しましょう。

そして、フックを利用しURLとパラメータを取得します。

```diff_tsx:/app/ui/invoices/pagination.tsx
'use client';
 
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
import Link from 'next/link';
import { generatePagination } from '@/app/lib/utils';
import { usePathname, useSearchParams } from 'next/navigation';
 
export default function Pagination({ totalPages }: { totalPages: number }) {
+ const pathname = usePathname();
+ const searchParams = useSearchParams();
+ const currentPage = Number(searchParams.get('page')) || 1;
 
  // ...
}
```

そして、次のページや前のページなどのURLが変化する場合に`createPageURL`という関数を呼び出します。

これによって、URLパラメータの`page`の部分を変更させています。

```
'use client';
 
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
import Link from 'next/link';
import { generatePagination } from '@/app/lib/utils';
import { usePathname, useSearchParams } from 'next/navigation';
 
export default function Pagination({ totalPages }: { totalPages: number }) {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const currentPage = Number(searchParams.get('page')) || 1;
 
  const createPageURL = (pageNumber: number | string) => {
    const params = new URLSearchParams(searchParams);
    params.set('page', pageNumber.toString());
    return `${pathname}?${params.toString()}`;
  };
 
  // ...
}
```

そして、前回実装した`handleSearch`関数のところに、ページ番号を１にリセットする処理を書きましょう。

```diff_tsx:/app/ui/search.tsx
'use client';
 
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useDebouncedCallback } from 'use-debounce';
 
export default function Search({ placeholder }: { placeholder: string }) {
  const searchParams = useSearchParams();
  const { replace } = useRouter();
  const pathname = usePathname();
 
  const handleSearch = useDebouncedCallback((term) => {
    const params = new URLSearchParams(searchParams);
+    params.set('page', '1');
    if (term) {
      params.set('query', term);
    } else {
      params.delete('query');
    }
    replace(`${pathname}?${params.toString()}`);
  }, 300);

```

# おわりに

`useState`で管理しなくても実装できるものなんですね。

`heroicon`がちょくちょく出てくるからいつか調べよう。

# 次の記事

https://qiita.com/naoyuki2/items/04ffef203ae798f8c7bc