【React Native/firebase】firestoreのページネーションを使ってみた
作っている「セワシタ?」アプリでチャットルームの機能を提供しています。
ユーザー(家族)同士で会話したり、誰かがお世話を実施したことを登録すると、ペットが「お世話してくれてありがとう!」と言ってくれる機能です。
(将来的にはLINE連携にしたいのですが、今はまだできていません)
ページネーション(pagenation)とは
複数件のデータが取れる場面で、
- まずは最初の100件を取得し、
- ユーザーが希望したら(次へボタンを押す、ページの最後までくる、など)次の100件を表示する、
- それを繰り返すことで最初の取得件数を節約したり、ロード時間を短縮したり、
といったことをするための方法ですね。
これをチャットルームで、最新のメッセージ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件を取得していた場合も、うまく取得していないとあいだの一件が消えちゃったとか、そういうことになりかねません。
今回の「セワシタ?」アプリでは過去のメッセージの削除や更新は考慮せず、常に新しいメッセージの追加しかないという前提のため、以下のような仕組みとしました。
- snapshotについては最初の25件のみ設定する
- 追加のもの以降は常に通常のgetで取得する
- 取得したデータはstateにコピーを作成しそこに追記していく形とする
- 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を使ってます。
すごく簡単にチャット機能作れちゃうのすごいんですが、ちょっと癖がある・・・。
チャット機能だとたぶんこんな感じで最初の部分だけonSnapshot設定して追加されたデータは自分でマージって感じにするのが、データのロード量とかも少なく抑えられていいのかなぁという感じ。
もっといい方法思いつくかもしれませんが。
チャットみたいに常に最新のみ追加されるわけじゃないときのページネーションはとりあえず今は考えたくない。
本当にキレイに表示するなら、データのマージとかをゴリゴリ自分で頑張るしかないんだろうなぁ。。。
コメント
コメントを投稿