firestoreのonSnapshotを使う際に気を付けたいこと


firestoreのコレクションに対するonSnapshotの覚書です。

対象

firestoreにあるコレクションの追加・削除・変更をクライアントサイドにリアルタイムに反映したい場合、次のようにonSnapshotを使って変更をリッスンすることができます。

client.js
const db = firebase.firestore();

db.collection('todos')
    .onSnapshot(function (querySnapshot) {
        for (let change of querySnapshot.docChanges()) {
            if (change.type === 'added') {
                // データが追加された時
            }
            else if (change.type === 'modified') {
                // データが変更された時
            }
            else if (change.type === 'removed') {
                // データが削除された時
            }
        }
    })

注意したいケース

最初の呼び出し

公式にも記載されていますが、onSnapshotを実行すると最初に全てのドキュメントを取得します。この時、change.typeaddedでコールされます。これは仮にfirestoreのコレクションが空の場合でも、関数自体の呼び出しは発生します。

client.js

db.collection('todos')
    .where('user', '==', user)
    .onSnapshot(function (querySnapshot) {
        for (let change of querySnapshot.docChanges()) {
            if (change.type === 'added') {
                // データが追加された時
                // もしくは最初の呼び出し
            }
            else if (change.type === 'modified') {
                // データが変更された時
            }
            else if (change.type === 'removed') {
                // データが削除された時
            }
        }
    })

limitクエリとの併用

onSnapshotはコレクションに対するクエリと併用ができます。
特に、limitorderByのようなクエリを併用する場合に注意が必要です。
例えば、以下のクエリを実行するとします。

client.js
// 作成日付が新しいものから5タスク購読する
db.collection('todos')
    .where('user', '==', user)
    .orderBy('createDate','desc')
    .limit(5)
    .onSnapshot(function (querySnapshot) {
        
    })

firestoreには以下のデータが入っていると想定します。

todos.json
{
 {"createDate":"2020/1/20", "title":"テスト1"},
 {"createDate":"2020/1/19", "title":"テスト2"},
 {"createDate":"2020/1/18", "title":"テスト3"},
 {"createDate":"2020/1/17", "title":"テスト4"},
 {"createDate":"2020/1/16", "title":"テスト5"},
 {"createDate":"2020/1/15", "title":"テスト6"},
}

onSnapshot実行直後に、テスト1〜テスト5の5つのTODOが取得できます。これは想定通りです。

client.js

    .onSnapshot(function (querySnapshot) {
        for (let change of querySnapshot.docChanges()) {
            if (change.type === 'added') {
                //テスト1〜テスト5が入ってくる
            }
            
        }
    })

次に新しいTODOをfirestoreに投入します。

todos.json
{
 {"createDate":"2020/1/21", "title":"テスト0"},
}

すると、onSnapshotではlimit(5)によって購読するデータの入れ替えが発生し、change.typeaddedremovedが呼び出されます。

client.js

db.collection('todos')
    .where('user', '==', user)
    .orderBy('createDate','desc')
    .limit(5)
    .onSnapshot(function (querySnapshot) {
        for (let change of querySnapshot.docChanges()) {
            if (change.type === 'added') {
                // テスト0が入ってくる
            }
            else if (change.type === 'modified') {

            }
            else if (change.type === 'removed') {
                // テスト5が入ってくる
            }
        }
    })

考えてみれば、テスト0の追加によって、5つの購読対象の中からテスト5が押し出されるというのは自然なことなのですが、limitを使用しない時の感覚で使ってしまうと、removedによってあたかもstoreから削除されたと判断しかねないため、注意が必要です。

同様に、次のデータの中からテスト3を削除してみます。

todos.json
{
 {"createDate":"2020/1/20", "title":"テスト1"},
 {"createDate":"2020/1/19", "title":"テスト2"},
 {"createDate":"2020/1/18", "title":"テスト3"},
 {"createDate":"2020/1/17", "title":"テスト4"},
 {"createDate":"2020/1/16", "title":"テスト5"},
 {"createDate":"2020/1/15", "title":"テスト6"},
}

こちらの結果はもう予想がつくかもしれませんが、テスト3が5つの購読リストから除外されたために、テスト6が代わりに購読対象に加わります。

client.js

db.collection('todos')
    .where('user', '==', user)
    .orderBy('createDate','desc')
    .limit(5)
    .onSnapshot(function (querySnapshot) {
        for (let change of querySnapshot.docChanges()) {
            if (change.type === 'added') {
                // テスト6が入ってくる
            }
            else if (change.type === 'modified') {

            }
            else if (change.type === 'removed') {
                // テスト3が入ってくる
            }
        }
    })

所感

limitクエリは、Infinite Scrollのように少しずつリッスンしたいときなどに考慮が必要かなと。