【React Native/firebase】firestoreのページネーションを使ってみた

作っている「セワシタ?」アプリでチャットルームの機能を提供しています。



ユーザー(家族)同士で会話したり、誰かがお世話を実施したことを登録すると、ペットが「お世話してくれてありがとう!」と言ってくれる機能です。



(将来的にはLINE連携にしたいのですが、今はまだできていません)



ページネーション(pagenation)とは



複数件のデータが取れる場面で、




  1. まずは最初の100件を取得し、

  2. ユーザーが希望したら(次へボタンを押す、ページの最後までくる、など)次の100件を表示する、

  3. それを繰り返すことで最初の取得件数を節約したり、ロード時間を短縮したり、




といったことをするための方法ですね。



これをチャットルームで、最新のメッセージ25件だけ表示し、過去分はボタンをクリックしたら表示する、という機能を追加しました。



firestoreでのページネーション



firestoreにはページネーションを実現するための機能が提供されています。



まずデータを取得する際に「limit」で件数を指定することができます。



今回のチャットアプリではメッセージの登録日時を持っているため、登録日時の新しい順に25件取得する、ということをします。



chatCollection.orderBy('createdAt', 'desc').limit(25).onSnapshot(async snapshot => {
this.setState({
lastMessage: snapshot.docs[snapshot.docs.length - 1] ,
});



※最後の一件をstateに退避しておきます



その後、次の25件を取得したいとユーザーが指示した場合、先ほど取得した25件の最後のものを指定して、「startAfter」を使用します。



let next = chatCollection.orderBy('createdAt', 'desc')
.startAfter(this.state.lastMessage!.data().createdAt)
.limit(25);



stateに退避しておいたlastMessageのcreatedAtを指定して「startAfter」することで、指定された「createdAd」より後のデータが取得できます。



ちなみに今回は並べ替えのキーがcreatedAt(つまり日時)のみでしたが、複数項目で並べ替えをする場合は、startAfterにはorderByした項目をその順番で指定してやります。



複数指定については公式ドキュメントも参照してください。



クエリカーソルを使用したデータのページ設定  |  Firebase



snapshotを使った時の問題点



firestoreのsnapshotを使うと、データに変更があった際に通知してくれ、画面の更新ができます。



ただ、snapshotとlimitを同時に使うと問題も発生します。



はじめ、25件取得した後で、新しいメッセージが投稿されたとしましょう。



すると、新しく1件追加されると同時に、25件目だったデータは26件目になってしまうため取得されるデータから消えます。



これをそのまま画面に表示していると、一件新しいメッセージが来たら、一件古いメッセージが消えた、みたいなことになります。



また、追加の25件を取得していた場合も、うまく取得していないとあいだの一件が消えちゃったとか、そういうことになりかねません。



今回の「セワシタ?」アプリでは過去のメッセージの削除や更新は考慮せず、常に新しいメッセージの追加しかないという前提のため、以下のような仕組みとしました。




  1. snapshotについては最初の25件のみ設定する

  2. 追加のもの以降は常に通常のgetで取得する

  3. 取得したデータはstateにコピーを作成しそこに追記していく形とする

  4. snapshotで取得された新規メッセージはunshiftでstateのメッセージの1番前に追加する




最終的にはデータ取得する箇所はこんな感じになりました。



   /** チャットのメッセージ取得 */
componentDidMount = async () => {
// コレクションの取得
const chatCollection = Fire.shared.chatCollection(store.familyId);

// 最初の25件のみ取得、onSnapshotを設定し変更を受信する
const chatUnsubscribe = chatCollection.orderBy('createdAt', 'desc').limit(25).onSnapshot(async snapshot => {

// stateのメッセージをコピー
const newMessages = this.state.messages.slice();

// snapshotのうち変更のあった行のみ取得(docChanges)
// 25件取得したものを逆順(reverse)に取得して一番前に追加(unshift)することで
// 複数件更新を受信した場合にも対応する
// (通信状態が悪く、一度に2件以上のメッセージを受信した場合を想定)
snapshot.docChanges().reverse().forEach(async chatMessage => {

// 追加されたメッセージのみ対象
if (chatMessage.type === 'added') {
const message = new ChatMessage(chatMessage.doc);

// unshiftで一番前に追加
newMessages.unshift(message);
}
});

this.setState({
// 初回以降はonSnapshotの中では最後の一件を設定しない
// pagenation対応のため
lastMessage: this.state.firstFlg ? snapshot.docs[snapshot.docs.length - 1] : this.state.lastMessage,
// チャットのメッセージ配列を設定
messages: newMessages,
// 最初の一回の時のみtrue、以降はコンポーネントが再ロードされるときまでずっとfalse
firstFlg: false,
});
});
Fire.shared.unsubscribeFunctions.push(chatUnsubscribe);
}

/** 過去のメッセージを読み込む */
getOlderMessage = () => {

// react-native-gifted-chat用の設定
this.setState({ isLoadingEarlier: true });

const chatCollection = Fire.shared.chatCollection(store.familyId);

let next = chatCollection.orderBy('createdAt', 'desc')
.startAfter(this.state.lastMessage!.data().createdAt) // 前回の最後のデータの日時
.limit(25);

// 過去のデータの更新はないため、通常のgetで取得する(onSnapshotしない)
next.get().then((snapshot) => {
const newMessages = this.state.messages.slice();
snapshot.forEach(async chatMessage => {
const message = new ChatMessage(chatMessage);
newMessages.push(message);
});
this.setState({
lastMessage: snapshot.docs[snapshot.docs.length - 1], // 新しい最後の一件を設定
messages: newMessages,
isLoadingEarlier: false, // react-native-gifted-chat用の設定
});

})
}



ちなみに、チャット部分にはreact-native-gifted-chatを使ってます。



すごく簡単にチャット機能作れちゃうのすごいんですが、ちょっと癖がある・・・。



github.com



チャット機能だとたぶんこんな感じで最初の部分だけonSnapshot設定して追加されたデータは自分でマージって感じにするのが、データのロード量とかも少なく抑えられていいのかなぁという感じ。



もっといい方法思いつくかもしれませんが。



チャットみたいに常に最新のみ追加されるわけじゃないときのページネーションはとりあえず今は考えたくない。



本当にキレイに表示するなら、データのマージとかをゴリゴリ自分で頑張るしかないんだろうなぁ。。。



コメント

人気の投稿