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
blitz へのログイン
blitz では、デフォルトで認証の仕組みが存在しますので、 Sign Up
ボタンをクリックしてアカウントを作成し、ログインします。
アプリケーションの開発
それではログインを確認できたところで、アプリケーション開発を行なっていきます。
モデルとページの作成
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
すでにページネーション( Previous
と Next
ボタン)が付いています。
データの登録
Blitz には、Prisma.js が入っていますので、ここでは Prisma Studio を使ってデータを登録します。
以下のコマンドで、Prisma Studio を起動します。
blitz prisma studio
以下の URL にアクセスすると、Prisma Studio が表示されます。
http://localhost:5555
先ほど作成した、Schedule モデルができているので、モデル名(Schedule)をクリックします。
Add record
ボタンをクリックすると、スプレッドシートの要領でデータ入力できますので、 Save & changes
ボタンをクリックして、データを登録します。
今回登録したデータは以下です。
title | date |
---|---|
データ1 | 2022-11-01 |
データ2 | 2022-11-02 |
データ3 | 2022-12-01 |
データ4 | 2022-12-02 |
データ5 | 2022-11-29 |
データ6 | 2022-11-30 |
作成されたページの修正
Blitz のコード生成ツールで生成されるページは、テンプレートを使用して作成されています。このテンプレートは、 name
という要素があるモデルを想定して作られているので、generate コマンド実行時の title
と date
は反映されていません。
そのため、以下のファイルを修正します。
- 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
Next
ボタンを押すと2ページ目に遷移します。
おっと、データのソート順が 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,
})
無事に日付順にソートされてページが表示されました。
本題の無限リストの実装
現在のページネーションを無限リスト方式に修正します。
無限リストクエリのリファレンスを参考に
以下のファイルを修正します。
- 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
に変更
修正箇所2:データの取得部分
変更箇所3:リスト部分の表示
変更箇所4:ボタン部分の表示
無限リストの実装の内容確認
以下の URL にアクセスして、確認してみます。
http://localhost:3000/schedules
Load More
をクリックして2ページ目を表示
あれ、また ソート順が日付順になっていません。
先ほどの反省を活かして、 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,
}
無限リストの実装を再確認
日付順に無限リストが表示されました。
まとめ
Blitz で、無限リストの実装を行いました。
途中、コード生成コマンドのテンプレート仕様と異なる部分のみ、書き換えが必要でしたが、最小限の編集で要件が満たせるアプリケーションができました。
このスピード感は、まさに Blitz(電撃的)です。
弊社でも Blitz を一部のサービスで使用しており、今後の発展を期待しています。