yourmystar tech blog
著者: nishino 公開日:

Blitz.js による無限リストの作り方

この記事はユアマイスター アドベントカレンダー 2022の7日目の記事です。

Blitz.js とは

  • Blitz.js(以下、Blitz)は、フロントエンド開発(React.js)からバックエンド開発(Prisma.js)までを統合して開発可能なフルスタックの Web アプリケーションです。
  • フロントエンド部分のフレームワークに Next.js を使用しており、ルーティングをファイルパスベースで実装することが可能です。
  • フロントエンドからバックエンドへの呼び出しに、独自の RPC(BlitzRPC)を使用しており、あたかも内部の関数を呼ぶようにバックエンドの API を呼び出せるので、フロントエンド開発者でもバックエンドを含めた開発を行いやすく、DX(開発者体験)が良いと感じます。
  • 認証処理、API 前処理用のミドルウェア、コード生成、ライブラリインストール用のレシピといったツールが揃っており、実践投入可能な安定したフレームワークと言ってよいと思います。
  • 公式ページ:https://blitzjs.com/

Blitz の歴史

Blitz は、2020 年 2 月 17 日に、Brandon Bayer(ブランドン・ベイヤー)が、フロントエンドのエコシステム(React.js)に Ruby on Rails のような フルスタックフレームワークがないことを理由に、Blitz の開発を発表しました。

登場して、まだ3年に満たない比較的新しいフレームワークになります。

Blitz の初期バージョンは、Next.js にビルトインした状態で開発されました。

当時、ブランドンには数百行のプロトタイプコードしかありませんでしたが、このアイデアは非常に多くの共感を呼んだため、多くの人が参加し、Blitz コミュニティが構成されました。

2020 年 4 月 24 日にリリースされた最初のアルファ版の時には、すでに 38 人のコントリビューターがいました。

開発発表からちょうど1年の 2021 年 2 月 17 日にベータ版(0.30.0)がリリースされました。

この後、ブランドンは Next.js をフォークする決断をします。Blitz の開発は、内部に存在する Next.js の開発に追従して、Next.js 部分をアップデートしながら開発されましたが、徐々にアップデートについていくことが難しくなり、互換性を担保する作業に追われたからです。

2021 年 12 月 18 日に、Blitz をフレームワークにとらわれないツールキットにピボットする提案が行われました。Blitz は、フロントエンド(Next.js)とバックエンド(Prisma.js)をつなぐユーティリティ部分の開発を行なっていたため、その部分に特化したツールキットとなる道を選んだのです。

これを発表した際に、tRPC の開発者から、tRPC を Blitz に取り込みたいと提案がありましたが、インターフェースの実装思想の違いから、決別しています。

2022 年 6 月に、ピボット後のアルファ版(v2.0.0-alpha.49)がリリースされました。Blitz と Next.js は分離し、独立したツールキットになりました。

2022 年 8 月に、ピボット後のベータ版(v2.0.0-beta.1)がリリースされました。

Blitz の年表

日付出来事
2020 年 2 月 17 日ブラントン・ベイヤーが Blitz の開発を発表
2020 年 4 月 24 日アルファ版(v0.6.0)をリリース
2021 年 2 月 17 日ベータ版(v0.30.0)をリリース
2021 年 12 月 18 日ツールキットへのピボットを提案(RFC Important Discussion On Possible Blitz Pivot)
2021 年 12 月 19 日tRPC との協議(RFC New Blitz Toolkit Standalone Zero API Data Layer)
2022 年 4 月 12 日Next.js を包含する最後のバージョン(v0.45.4)をリリース
2022 年 6 月 5 日ピボット後のアルファ版(v2.0.0-alpha.49)をリリース
2022 年 8 月 15 日ピボット後のベータ版(v2.0.0-beta.1)をリリース
2022 年 11 月 23 日執筆時の最新版(v2.0.0-beta.19)のリリース

本題

今回は、Blitz.js における無限リスト方式のページネーションの実装を行なってみたいと思います。

この記事でできること

日付順のリストを用意し、初期表示は 10 件だけ表示し、以降は読み込みボタンをクリックすると次のデータを表示する無限リストのページネーションを実装します。

準備

それでは、環境のセットアップから行います。

blitz のインストール

以下のコマンドで、blitz をインストールします。

npm install -g blitz
# または、yarn global add blitz

今回は、以下の Blitz バージョンを使用しています。(執筆時の最新版)

$ blitz -v
Blitz version: 2.0.0-beta.19

blitz アプリケーションの作成

以下のコマンドで、blitz アプリケーションを作成します。

$ blitz new your-blitz-app
# "your-blitz-app" は任意のアプリケーション名を指定します。

コマンドを実行すると、インタラクティブに環境についての選択肢が表示されるので、自分の環境に合わせて選択します。

blitz の起動

以下のコマンドで、blitz を起動します。

$ cd your-blitz-app
$ blitz dev

以下の URL にアクセスすると、blitz のトップ画面が表示されます。

http://localhost:3000

01_top.png

blitz へのログイン

blitz では、デフォルトで認証の仕組みが存在しますので、 Sign Up ボタンをクリックしてアカウントを作成し、ログインします。

02_create-account.png

アプリケーションの開発

それではログインを確認できたところで、アプリケーション開発を行なっていきます。

モデルとページの作成

Blitz にはコード生成ツール(generate コマンド)が用意されていますので、これを使ってモデルとページを生成します。

以下のコマンドを実行します。

今回は、title(文字列型)と date(日付型)を持つモデル(schedule)を作成します。

$ blitz generate all schedule title:string date:DateTime

途中で、以下のメッセージが表示されますので、「Y」を入力し、データベースのマイグレーションを実行します。

✔ Run 'prisma migrate dev' to update your database? (Y/n)

続いてマイグレーションのファイル名を求められるので入力します。(今回は、 create schedule と入力します)

✔ Enter a name for the new migration: … create schedule

作成されたページの確認

Blitz を再起動して、以下の URL にアクセスすると、上記 generate コマンドで生成されたページができています。

http://localhost:3000/schedules

03_list.png

すでにページネーション( PreviousNext ボタン)が付いています。

データの登録

Blitz には、Prisma.js が入っていますので、ここでは Prisma Studio を使ってデータを登録します。

以下のコマンドで、Prisma Studio を起動します。

blitz prisma studio

以下の URL にアクセスすると、Prisma Studio が表示されます。

http://localhost:5555

04_prisma-login.png

先ほど作成した、Schedule モデルができているので、モデル名(Schedule)をクリックします。

Add recordボタンをクリックすると、スプレッドシートの要領でデータ入力できますので、 Save & changes ボタンをクリックして、データを登録します。

05_prisma-studio.png

今回登録したデータは以下です。

titledate
データ12022-11-01
データ22022-11-02
データ32022-12-01
データ42022-12-02
データ52022-11-29
データ62022-11-30

作成されたページの修正

Blitz のコード生成ツールで生成されるページは、テンプレートを使用して作成されています。このテンプレートは、 name という要素があるモデルを想定して作られているので、generate コマンド実行時の titledate は反映されていません。

そのため、以下のファイルを修正します。

  • src/pages/schedules/index.tsx
$ git diff
diff --git a/src/pages/schedules/index.tsx b/src/pages/schedules/index.tsx
index 554ff57..e535d0c 100644
--- a/src/pages/schedules/index.tsx
+++ b/src/pages/schedules/index.tsx
@@ -6,8 +6,9 @@ import { usePaginatedQuery } from "@blitzjs/rpc"
 import { useRouter } from "next/router"
 import Layout from "src/core/layouts/Layout"
 import getSchedules from "src/schedules/queries/getSchedules"
+import { format } from "date-fns"

-const ITEMS_PER_PAGE = 100
+const ITEMS_PER_PAGE = 2

 export const SchedulesList = () => {
   const router = useRouter()
@@ -27,7 +28,9 @@ export const SchedulesList = () => {
         {schedules.map((schedule) => (
           <li key={schedule.id}>
             <Link href={Routes.ShowSchedulePage({ scheduleId: schedule.id })}>
-              <a>{schedule.name}</a>
+              <a>
+                {format(schedule.date, "yyyy-MM-dd")}:{schedule.title}
+              </a>
             </Link>
           </li>
         ))}

日付の表示を yyyy-MM-dd 形式にしたいので、以下のコマンドで date-fns をインストールします。

npm install date-fns

ページネーションの確認

それではこの状態で、ページネーションの動作を確認します。

以下の URL にアクセスして、ページネーションを確認してください。

http://localhost:3000/schedules

06_pagenation1-1.png

Next ボタンを押すと2ページ目に遷移します。

07_pagenation1-2.png

おっと、データのソート順が id のため、日付順に並んでいません。

以下のファイルの修正を行い、ソート順を日付順にします。

  • src/pages/schedules/index.tsx
diff --git a/src/pages/schedules/index.tsx b/src/pages/schedules/index.tsx
index e535d0c..a5cbacc 100644
--- a/src/pages/schedules/index.tsx
+++ b/src/pages/schedules/index.tsx
@@ -14,7 +14,7 @@ export const SchedulesList = () => {
   const router = useRouter()
   const page = Number(router.query.page) || 0
   const [{ schedules, hasMore }] = usePaginatedQuery(getSchedules, {
-    orderBy: { id: "asc" },
+    orderBy: { date: "asc" },
     skip: ITEMS_PER_PAGE * page,
     take: ITEMS_PER_PAGE,
   })

無事に日付順にソートされてページが表示されました。

08_pagenation1-3.png

本題の無限リストの実装

現在のページネーションを無限リスト方式に修正します。

無限リストクエリのリファレンスを参考に

useInfiniteQuery - Blitz.js

以下のファイルを修正します。

  • src/pages/schedules/index.tsx
diff --git a/src/pages/schedules/index.tsx b/src/pages/schedules/index.tsx
index e535d0c..213cd97 100644
--- a/src/pages/schedules/index.tsx
+++ b/src/pages/schedules/index.tsx
@@ -2,7 +2,7 @@ import { Suspense } from "react"
 import { Routes } from "@blitzjs/next"
 import Head from "next/head"
 import Link from "next/link"
-import { usePaginatedQuery } from "@blitzjs/rpc"
+import { useInfiniteQuery } from "@blitzjs/rpc"
 import { useRouter } from "next/router"
 import Layout from "src/core/layouts/Layout"
 import getSchedules from "src/schedules/queries/getSchedules"
@@ -13,35 +13,39 @@ const ITEMS_PER_PAGE = 2
 export const SchedulesList = () => {
   const router = useRouter()
   const page = Number(router.query.page) || 0
-  const [{ schedules, hasMore }] = usePaginatedQuery(getSchedules, {
-    orderBy: { date: "asc" },
-    skip: ITEMS_PER_PAGE * page,
-    take: ITEMS_PER_PAGE,
-  })
-
-  const goToPreviousPage = () => router.push({ query: { page: page - 1 } })
-  const goToNextPage = () => router.push({ query: { page: page + 1 } })
+  const [schedulePages, { isFetching, isFetchingNextPage, fetchNextPage, hasNextPage }] =
+    useInfiniteQuery(
+      getSchedules,
+      (page = { take: ITEMS_PER_PAGE, skip: 0, orderBy: { date: "asc" } }) => page,
+      {
+        getNextPageParam: (lastPage) => lastPage.nextPage,
+      }
+    )

   return (
     <div>
-      <ul>
-        {schedules.map((schedule) => (
-          <li key={schedule.id}>
-            <Link href={Routes.ShowSchedulePage({ scheduleId: schedule.id })}>
-              <a>
-                {format(schedule.date, "yyyy-MM-dd")}:{schedule.title}
-              </a>
-            </Link>
-          </li>
-        ))}
-      </ul>
+      {schedulePages.map((page, i) => (
+        <ul key={i}>
+          {page.schedules.map((schedule) => (
+            <li key={schedule.id}>
+              <Link href={Routes.ShowSchedulePage({ scheduleId: schedule.id })}>
+                <a>
+                  {format(schedule.date, "yyyy-MM-dd")}:{schedule.title}
+                </a>
+              </Link>
+            </li>
+          ))}
+        </ul>
+      ))}

-      <button disabled={page === 0} onClick={goToPreviousPage}>
-        Previous
-      </button>
-      <button disabled={!hasMore} onClick={goToNextPage}>
-        Next
+      <button onClick={() => fetchNextPage()} disabled={!hasNextPage || !!isFetchingNextPage}>
+        {isFetchingNextPage
+          ? "Loading more..."
+          : hasNextPage
+          ? "Load More"
+          : "Nothing more to load"}
       </button>
+      <div>{isFetching && !isFetchingNextPage ? "Fetching..." : null}</div>
     </div>
   )
 }

修正箇所1:ページネーションクエリを usePagenateQuery から useInfinityQuery に変更

09_code1-1.png

修正箇所2:データの取得部分

10_code1-2.png

変更箇所3:リスト部分の表示

11_code1-3.png

変更箇所4:ボタン部分の表示

12_code1-4.png

無限リストの実装の内容確認

以下の URL にアクセスして、確認してみます。

http://localhost:3000/schedules

13_infinity1-1.png

Load More をクリックして2ページ目を表示

14_infinity1-2.png

あれ、また ソート順が日付順になっていません。

先ほどの反省を活かして、 useInfiniteQuery のパラメータに orderBy: { date: "asc" } を入れているのになぜでしょう?

実は、useInfiniteQuery のパラメータで渡せるのは初期表示のみです。

2ページ目以降のパラメータは、API 内で適切に返却する必要があります。

getSchedules API を修正

以下のファイルを修正します。

  • app/schedules/queries/getSchedules.ts

最後のリターン値のnextPage パラメータに orderByを含めます。

nextPage は、最後のページの場合に null が設定されていますので、値がある場合のみ追加するように修正します。

diff --git a/src/schedules/queries/getSchedules.ts b/src/schedules/queries/getSchedules.ts
index 2c22c85..53f4195 100644
--- a/src/schedules/queries/getSchedules.ts
+++ b/src/schedules/queries/getSchedules.ts
@@ -23,7 +23,7 @@ export default resolver.pipe(

     return {
       schedules,
-      nextPage,
+      nextPage: nextPage ? { ...nextPage, orderBy } : nextPage,
       hasMore,
       count,
     }

無限リストの実装を再確認

日付順に無限リストが表示されました。

15_infinity2.png

まとめ

Blitz で、無限リストの実装を行いました。

途中、コード生成コマンドのテンプレート仕様と異なる部分のみ、書き換えが必要でしたが、最小限の編集で要件が満たせるアプリケーションができました。

このスピード感は、まさに Blitz(電撃的)です。

弊社でも Blitz を一部のサービスで使用しており、今後の発展を期待しています。

ツイートするはてなブックマークに追加シェアする