Blitz.js による無限リストの作り方(2)
この記事はユアマイスター アドベントカレンダー 2022の 14 日目の記事です
前回のおさらい
前回(アドベントカレンダー 2022 の 7 日目)の記事で、Blitz.js による無限リストを作りました。今回は、この無限リストを未来方向ではなく、過去方向にもリストを読み込むボタンを作ります。
Blitz.js とは何か?については前回の記事に記載していますので、そちらを参照してください。
この記事でできること
初期表示時に、今日から始まる 10 件のデータを表示し、「以後のデータを読み込む」ボタンをクリックすると「未来」のスケジュールが表示され、「以前のデータを読み込む」ボタンをクリックすると「過去」のスケジュールが表示されるリストを作ります。
無限リスト(未来および過去方向)の考え方
普段、私たちが SNS や Web サービスで、未来および過去方向へ進むリストについて、目にする機会がないので、軽く未来および過去方向へのリスト遷移について整理しておきます。
少なくとも私は、今のデータから未来および過去の両方に 10「件」分のデータを取得する UI を見たことがありません。(見たことがあるという方は、コメント欄にて教えてください。)
パッと思いつくのは、Google カレンダーなどのカレンダーアプリですが、
-
これは初めに今月(2022 年 12 月)のデータを表示し、前の月(2022 年 11 月)のデータを表示する場合は、2022 年 11 月を条件にして再検索して表示しています。
これは、検索対象の年月がわかっていればできますが、今日のデータから 10 件分古いデータを取得する場合は、前の月という検索条件では取得できません。
Twitter や Facebook も、基本的には未来の情報はないので、現在から過去への一方向の検索で済みます。
GCP のログエクスプローラは、それっぽい画面ですが、
-
このログエクスプローラも今のデータから 10 件といった検索ではなく、「1 時間延長」と記載されている通り、検索条件の範囲を現在の条件から 1 時間過去(または未来)に変更して再検索しているに過ぎません。
ではどうやったら、今のデータから 10 件分古いデータを取得するのでしょうか?
簡単です。
全データを日付順にソートし、今のデータが何件目かを確認した上で、その件数分をスキップして初期表示を行い、前の 10 件を表示する場合は、スキップする件数を 10 件減らして再検索を行います。
SQL でいう OFFSET
を使います。
参考:Prisma.js の Offset Pagination
無限リスト(未来および過去方向)の実装
それでは、無限リスト(過去方向)を実装していきます。
本日データを最初に表示するためにスキップが必要な件数を取得するクエリの作成
以下のファイルを追加します。
- src/schedules/queries/getScheduleSkipCount.tsx
import { resolver } from '@blitzjs/rpc'
import db, { Prisma } from 'db'
interface GetScheduleSkipCountInput extends Pick<Prisma.ScheduleFindManyArgs, 'where'> {}
export default resolver.pipe(resolver.authorize(), async ({ where }: GetScheduleSkipCountInput) => {
const count = await db.schedule.count({
where,
})
return count
})
このクエリは、引数のパラメータ( where
)を条件に、カウント数を返すクエリです。リストページでこの getScheduleSkipCount
を呼び出して、取得したカウント数分スキップし、本日以降のデータを初期表示します。
次に、以下のページファイルを修正します。
- src/pages/schedules/index.tsx
diff --git a/src/pages/schedules/index.tsx b/src/pages/schedules/index.tsx
index 213cd97..acb561c 100644
--- a/src/pages/schedules/index.tsx
+++ b/src/pages/schedules/index.tsx
@@ -2,21 +2,29 @@ import { Suspense } from "react"
import { Routes } from "@blitzjs/next"
import Head from "next/head"
import Link from "next/link"
-import { useInfiniteQuery } from "@blitzjs/rpc"
+import { useQuery, useInfiniteQuery } from "@blitzjs/rpc"
import { useRouter } from "next/router"
import Layout from "src/core/layouts/Layout"
+import getScheduleSkipCount from "src/schedules/queries/getScheduleSkipCount"
import getSchedules from "src/schedules/queries/getSchedules"
-import { format } from "date-fns"
+import { format, startOfToday } from "date-fns"
const ITEMS_PER_PAGE = 2
export const SchedulesList = () => {
const router = useRouter()
const page = Number(router.query.page) || 0
+ const [skipCount] = useQuery(getScheduleSkipCount, {
+ where: {
+ date: {
+ lt: startOfToday(),
+ },
+ },
+ })
const [schedulePages, { isFetching, isFetchingNextPage, fetchNextPage, hasNextPage }] =
useInfiniteQuery(
getSchedules,
- (page = { take: ITEMS_PER_PAGE, skip: 0, orderBy: { date: "asc" } }) => page,
+ (page = { take: ITEMS_PER_PAGE, skip: skipCount, orderBy: { date: "asc" } }) => page,
{
getNextPageParam: (lastPage) => lastPage.nextPage,
}
先ほど作成した getScheduleSkipCount
クエリを import し、 useQuery
のパラメータに指定します。where 句には「今日より前( lt
: less than startOfToday
)」を指定します。
startOfToday
は、date-fns のメソッドで今日の 0 時を表す Date 型を返します。
date-fns のリファレンス: https://date-fns.org/v2.29.3/docs/startOfDay
ここで一度画面を確認します。
以下の URL にアクセスします。
http://localhost:3000/schedules
本日(執筆している 12/04)のデータから表示されました。
前のスケジュールを読み込む処理の追加
次に、前のスケジュールを読み込む処理を追加します。
上記リファレンスに、useInfinityQuery の戻り値の情報があるので、これを元に過去方向の読み込みの実装を行います。
以下のファイルを修正します。
- src/pages/schedules/index.tsx
diff --git a/src/pages/schedules/index.tsx b/src/pages/schedules/index.tsx
index acb561c..142695b 100644
--- a/src/pages/schedules/index.tsx
+++ b/src/pages/schedules/index.tsx
@@ -21,17 +21,40 @@ export const SchedulesList = () => {
},
},
})
- const [schedulePages, { isFetching, isFetchingNextPage, fetchNextPage, hasNextPage }] =
- useInfiniteQuery(
- getSchedules,
- (page = { take: ITEMS_PER_PAGE, skip: skipCount, orderBy: { date: "asc" } }) => page,
- {
- getNextPageParam: (lastPage) => lastPage.nextPage,
- }
- )
+ const [
+ schedulePages,
+ {
+ isFetching,
+ isFetchingNextPage,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingPreviousPage,
+ fetchPreviousPage,
+ hasPreviousPage,
+ },
+ ] = useInfiniteQuery(
+ getSchedules,
+ (page = { take: ITEMS_PER_PAGE, skip: skipCount, orderBy: { date: "asc" } }) => page,
+ {
+ getNextPageParam: (lastPage) => lastPage.nextPage,
+ getPreviousPageParam: (firstPage) => firstPage.nextPage,
+ }
+ )
return (
<div>
+ <button
+ onClick={() => fetchPreviousPage()}
+ disabled={!hasPreviousPage || !!isFetchingPreviousPage}
+ >
+ {isFetchingPreviousPage
+ ? "Loading more..."
+ : hasPreviousPage
+ ? "Load Previous Schedule More"
+ : "Nothing more to load"}
+ </button>
+ <div>{isFetching && !isFetchingPreviousPage ? "Fetching..." : null}</div>
+
{schedulePages.map((page, i) => (
<ul key={i}>
{page.schedules.map((schedule) => (
@@ -50,7 +73,7 @@ export const SchedulesList = () => {
{isFetchingNextPage
? "Loading more..."
: hasNextPage
- ? "Load More"
+ ? "Load Next Schedule More"
: "Nothing more to load"}
</button>
<div>{isFetching && !isFetchingNextPage ? "Fetching..." : null}</div>
基本的には、前回追加した〜NextPage ( isFetchingNextPage
, fetchNextPage
, hasNextPage
)に対応する、〜PreviousPage( isFetchingPreviousPage
, fetchPreviousPage
, hasPreviousPage
)という関数があるので、同じように実装します。
過去方向の読み込みボタン( Load Previous Schedule More
)が表示されました。
(合わせて未来方向のボタンの表示を修正しています)
過去方向の読み込みボタンを押してみます。
なんと、過去のデータではなく、未来のデータが表示されてしまいました。
これは、 getPreviousPageParam
に渡している関数に nextPage
を返すように設定しているのが悪そうです。(リファレンスにも nextPage
が記載されているのですが、記載ミスでしょうか?)
getPreviousPageParam: (firstPage) => firstPage.nextPage,
nextPage
ではなく、 previousPage
という関数があれば良いのですが、リファレンスおよび blitz のコードを見ても存在しません。
// node_modules/blitz/dist/index-server.d.ts:143-155
declare function paginate<QueryResult>({ skip, take, maxTake, count: countQuery, query, }: PaginateArgs<QueryResult>): Promise<{
items: Awaited<QueryResult>;
nextPage: {
take: number;
skip: number;
} | null;
hasMore: boolean;
pageCount: number;
pageSize: number;
from: number;
to: number;
count: number;
}>;
仕方がないので、previousPage に該当する処理を実装します。
diff --git a/src/pages/schedules/index.tsx b/src/pages/schedules/index.tsx
index 142695b..d9dfcce 100644
--- a/src/pages/schedules/index.tsx
+++ b/src/pages/schedules/index.tsx
@@ -21,6 +21,7 @@ export const SchedulesList = () => {
},
},
})
+ const orderBy = { date: "asc" }
const [
schedulePages,
{
@@ -34,10 +35,20 @@ export const SchedulesList = () => {
},
] = useInfiniteQuery(
getSchedules,
- (page = { take: ITEMS_PER_PAGE, skip: skipCount, orderBy: { date: "asc" } }) => page,
+ (page = { take: ITEMS_PER_PAGE, skip: skipCount, orderBy }) => page,
{
getNextPageParam: (lastPage) => lastPage.nextPage,
- getPreviousPageParam: (firstPage) => firstPage.nextPage,
+ getPreviousPageParam: (firstPage) => {
+ if (!firstPage.nextPage) return null
+ const skip = Math.max(firstPage.nextPage.skip - ITEMS_PER_PAGE * 2, 0)
+ if (skip === 0) return null
+ const take = skip < ITEMS_PER_PAGE ? skip : ITEMS_PER_PAGE
+ return {
+ take,
+ skip,
+ orderBy,
+ }
+ },
}
)
やったことは以下です。
- getPreviousPageParam の戻り値に orderBy を追加したいので orderBy を変数化。
- firstPage.nextPage には、firstPage の次のデータを表示するための skip 数が入っているため、そこから
ITEMS_PER_PAGE * 2
を引き算します。
- 引き算の結果、マイナスになる可能性があるので、その場合は、skip = 0 になるよう
Math.max
で比較します。 - 過去のデータがもうない場合は、
getPreviousPageParam
がnull
を返せば、hasPreviousPage
もfalse
になる仕様があるので、null
を返します。 - 過去のデータがまだある場合は、
take
とskip
およびorderBy
を返しますが、skip
の数が、1 ページの表示数より少ない場合、skip
数分だけ取得するようにtake
を調整します。
無限リスト(未来および過去方向)の実装を再確認
過去方向の読み込みボタン( Load Previous Schedule More
)をクリックすると、過去のスケジュールが読み込まれ、
未来方向の読み込みボタン(Load Next Schedule More
)クリックすると、未来のスケジュールが読み込まれました。
まとめ
未来および過去方向に読み込みを行う無限リストを実装しました。
Blitz.js には 無限リストを扱うライブラリがあるため、比較的容易に実装することができました。過去方向のデータを取得する際は、スキップの件数を調整することによって、取得するデータを調整します。