TypescriptでFirestoreのページネーションを実装する

FirestoreをデータストアとしたNext.jsのアプリケーションを作ってみています。Firestoreのあるコレクションのデータを一覧表示したときに、ページネーション機能が必要だなと思い、作ってみようとしたところ、思いの外苦戦したので、実現方法を纏めておきます。

Firestoreのページネーション

まずはじめにFirestoreには、SQLでいうところのLIMITはあるのですが件数を基準にしたOFFSETに相当する機能がないので、

[<< ] [<] [1] [2] [3] ... [>] [>>]

このようなページネーションは簡単にはできなさそうです。

代わりにクエリカーソルを使ってのページ設定というのが可能なので、

[< prev] [next >]

このようなページネーションであれば実現できそうです。 クエリカーソルを使ったページ設定では、具体的には、

  • クエリの開始点
  • クエリの終了点

と件数を指定することで、取得したいデータの範囲を絞ってページネーションを実現することになります。

クエリの開始点の指定

クエリの開始点を指定するには、startAt()startAfter()メソッドを使用します。 両者の違いは、引数に指定した点を含めるか否か、で、後者の場合は指定した点の次のデータからが取得範囲になります。

クエリの終了点の指定

クエリの終了点を指定するには、endAt()endBefore()メソッドを使用します。 開始点と同じように両者の違いは指定した点を含めるか否か、で、後者の場合は指定した点の前のデータからが取得範囲になります。

取得件数の指定

取得したいデータの件数を指定するには、limit()limitToLast()メソッドを使用します。

limit()はクエリにヒットするデータの先頭から指定した件数だけを取得できます。 limitToLast()はクエリにヒットするデータの末尾から指定した件数だけを取得できます。

実際のページネーション

したがって、実際のページネーションは上記を組み合わせて実現します。 具体的には、次のような形になります。

先頭ページを取得する場合

先頭ページを取得する場合は、クエリーカーソルは使いません。 このような関数でデータが取得できます。

const getSnapshot = async (perPage: number): Promise<firebaseClient.firestore.QuerySnapshot<FirebaseFirestore.DocumentData>> => {
  let query: firebaseClient.firestore.Query<FirebaseFirestore.DocumentData> = firestore.collection(collectionName).orderBy(sortField);
  query = query.limit(perPage);
  return await query.get();
}

ここで、collectionNameはFirestoreのコレクションの名称、perPageは1ページに含めるデータの最大数、sortFieldはデータを並び替えるフィールドの名前になります。 つまり、collectionNameで指定したコレクションを、sortFieldでソートして、先頭から最大perPage件取得することになります。

次ページを取得する場合

とあるページを表示中に、そのページの次のページを取得する場合のクエリは次のようになります。

const getNextSnapshot = async (start: string, perPage: number): Promise<firebaseClient.firestore.QuerySnapshot<FirebaseFirestore.DocumentData>> => {
  let query: firebaseClient.firestore.Query<FirebaseFirestore.DocumentData> = firestore.collection(collectionName).orderBy(sortField);
  query = query.startAfter(start).limit(perPage);
  return await query.get();
}

次ページを取得する場合は、先程出てきたstartAfter()limit()を使います。 startAfter()には、現在表示中のページの最後のデータの値、orderBy()に指定したフィールドの値を指定します。上のコードでは、startパラメーターがそれに当たります。startAfter()で指定した開始点から最大でlimit()で指定した件数取得する、という動きになります。

前ページを取得する場合

とあるページを表示中に、そのページの前のページを取得する場合のクエリは次のようになります。

const getPrevSnapshot = async (end: string, perPage: number): Promise<firebaseClient.firestore.QuerySnapshot<FirebaseFirestore.DocumentData>> => {
  let query: firebaseClient.firestore.Query<FirebaseFirestore.DocumentData> = firestore.collection(collectionName).orderBy(sortField);
  query = query.endBefore(end).limitToLast(perPage);
  return await query.get();
}

次ページを取得する場合は、先程出てきたendBefore()limitToLast()を使います。 endBefore()には、現在表示中のページの先頭のデータの値、orderBy()に指定したフィールドの値を指定します。上のコードでは、endパラメーターがそれに当たります。endBefore()で指定した終了点から最大でlimitToLast()で指定した件数取得する、という動きになります。

ここで、limitToLast()ではなくlimit()を使ってしまうと、終了点を指定しても先頭データからデータを取得してしまうことになり、期待通りのデータが取得できないので注意が必要です。

参考