Remix + Prisma でページング処理を実装してみた

## はじめに

今回は、[Remix](https://remix.run/docs/en/main) と [Prisma](https://www.prisma.io/docs/orm) を使って簡単なページング処理を実装してみました。UI には Tailwind CSS ベースのコンポーネントライブラリである [Flowbite React](https://flowbite-react.com/docs/getting-started/introduction) を使用しています。すべてのデータを一度に取得してフロントエンドでフィルターするのではなく、ページが切り替わるたびにデータベースから必要なデータを取得するようにしています。

なお、今回実装した内容は以下のリポジトリに置いてありますので、必要に応じて確認してみてください。

https://github.com/ore88ore/remix-paging-example

## 環境

- Remix: `2.11.0`
- Tailwind CSS: `3.4.4`
- Flowbite React: `0.10.1`
- Prisma: `5.17.0`

## プロジェクトをセットアップ

### Remix + Tailwind CSS

```zsh
% npx create-remix@latest
```

この状態でローカルサーバーを起動すると `Welcome to Remix` が表示される状態となります。

```zsh
% npm run dev
```

また、Tailwind CSS も Vite の [PostCSS](https://vitejs.dev/guide/features#postcss) を使ってセットアップ済みとなっていました。手動でセットアップする場合は、[こちらの公式ドキュメント](https://remix.run/docs/en/main/styling/tailwind)が参考になります。

```zsh
% tree
.
├── app
├── └── tailwind.css
├── postcss.config.js
├── tailwind.config.ts
...
```

### Flowbite React

Flowbite React パッケージをインストールします。

```zsh
% npm i flowbite-react
```

`tailwind.config.ts` の `content` と `plugin` に Flowbite React の設定を追加します。

```typescript
import flowbite from "flowbite-react/tailwind";  // 追加
import type { Config } from "tailwindcss";

export default {
  content: [
    "./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}",
    flowbite.content(),  // 追加
  ],
  theme: {
    extend: {},
  },
  plugins: [
      flowbite.plugin()  // 追加
  ],
} satisfies Config;
```

### Prisma

Prisma パッケージをインストールと Prisma schema 関連ファイルを生成します。

```zsh
% npm install prisma --save-dev
...
% npx prisma init
```

## 実装

各種パッケージの利用準備が整ったので、実装していきます。

### UI

UI は Tailwind CSS ベースの Flowbite React を利用してみました。いちからページングの UI を実装するのに比べて提供されているコンポーネントを利用するほうが簡単に実装することができます。今回はページング処理を実装したいので、ページング処理部分に [Pagination](https://flowbite-react.com/docs/components/pagination) コンポーネント、データ表示部分に [Table](https://flowbite-react.com/docs/components/table) コンポーネントを利用して、サクッと UI を作成しました。

![Monosnap Image 2024-08-03 17-50-30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331769/a6c91ca4-f608-1aee-0458-773cccf08a14.png)

Pagination コンポーネントは、必要なプロパティを渡してあげると、アクティブなページの表示や前のページ、次のページの制御、移動可能なページの表示などページング処理に必要な UI 側の制御をやってくれて、とても楽に組み込むことができました。

### データ取得のタイミング

Remix でデータの取得処理は、[Loader](https://remix.run/docs/en/main/route/loader) を使うことができるので、Loader でデータを取得するようにします。ただ、初期データだけなら、Loader でデータを取得するだけでよいのですが、今回はページ番号によってデータを取得する必要があるので、シンプルにクエリパラメータに `page` パラメータを追加することにしました。こうすることで URL でページ番号を指定できるようになります。直接特定のページを表示したい場合やブラウザバックなどもいい感じにしてくれて、良さそうですね。

```ts
export const loader = async ({ request }: LoaderFunctionArgs) => {
  const url = new URL(request.url);
  const pageNumber = parseInt(url.searchParams.get("page") ?? "1");
  const pageSize = 10;

  const { totalCount, items } = await fetchPagedItems(pageNumber, pageSize);
  const totalPages = Math.ceil(totalCount / pageSize);

  return json({ totalPages, pageNumber, items });
};
```

### データ取得処理

ページを切り替えた際に、対象のデータのみ取得するように Prisma ORM を使って実装しています。実装のサンプルは以下のようにしています。

```ts
const fetchPagedItems = async (pageNumber: number, pageSize: number) => {
  const prisma = new PrismaClient();
  const totalCount = await prisma.item.count();
  const items = await prisma.item.findMany({
    skip: (pageNumber - 1) * pageSize,
    take: pageSize,
    orderBy: {
      id: "asc",
    },
  });

  return { totalCount, items };
};
```

[findMany](https://www.prisma.io/docs/orm/reference/prisma-client-reference#findmany) の `skip` と `take` を使って、オフセットベースのページネーションで実装しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331769/dbfa5ab3-24ba-ff34-8340-4eecdb19aac4.png)
（※ [Prisma/docs](https://www.prisma.io/docs/orm/prisma-client/queries/pagination#offset-pagination) より）

[Offset pagination](https://www.prisma.io/docs/orm/prisma-client/queries/pagination#offset-pagination) は、どのページにもジャンプすることができ、任意の項目でソートすることができる点がメリットとなります。ただし、表示対象のデータが大量の場合の利用には向いていないようです。
公式ページで紹介されていますが、[Cursor-based pagination](https://www.prisma.io/docs/orm/prisma-client/queries/pagination#offset-pagination) のように Cursor を使ったページネーションもありますので、ユースケースに応じて、実装方法を選択するのが良さそうですね。

## 動作確認

選択したページに応じたデータが取得・表示されていることを確認できました。前後にページが存在する場合は、表示中のページ番号が真ん中に配置されるようになっているようです。ここらへんの制御をコンポーネント ([Pagination](https://flowbite-react.com/docs/components/pagination)) がよしなにやってくれるのもいいですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331769/c116700a-c70a-2656-5634-87ac47b7cd8b.png)

今回のように、ページング処理の際に Remix の [useNavigate](https://remix.run/docs/en/main/hooks/use-navigate) を使ってページを遷移させた際は、内部的には Fetch リクエストが利用されていることを確認できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331769/c3578801-60cc-13d0-f79d-5b8113b3d436.png)

## 参考

https://remix.run/docs/en/main/route/loader

https://www.prisma.io/docs/orm/prisma-client/queries/pagination

https://flowbite-react.com/docs/components/pagination