👂

VueでFirestoreのSubscribe機能を使いたい時のアプローチ

2020/09/26に公開

はじめに

VueでFirestoreのSubscribe機能を使ってリアルタイム処理を行いたい時に、いくつかアプローチがあるのですが、それをそれぞれ検証してどうだったかを記録として残しています。

同じように、Vue・FirestoreでSubscribe機能を使いたい人の参考になれば嬉しいです。

今回、検証に使用した環境は以下です。

  • Nuxt.js v2.4.0
  • TypeScriptは未使用

FirestoreのSubscribe機能について

大まかな手順として、

  1. リアルタイム検知したいFirestoreのドキュメントを指定して、監視対象として登録
    2. Vueであれば、mounted時に登録すればよい
    3. Firestoreのドキュメント・コレクションあたりを知らない場合は、こちらを参照されたし
    4. https://firebase.google.com/docs/firestore/data-model?hl=ja
  2. 別の画面への遷移などのタイミングで、subscribe登録したFirestoreのドキュメントを監視対象からはずす(unsubscribeする)
    4. Vueであれば、beforeDestroy時に登録を外せばよい

前提となるデータ

  • 以下のようなTODOタスクのデータが、Firestoreにあることを前提とします。
    • Collectionが todostitle,content,completedというキーバリューを持ったDocumentがある状態です

各アプローチの検証・解説

Vueコンポーネントのmount・unmountフック時にFirestoreのsubscriptionを実装する

メリット

  • Vueだけで完結する一番手軽なアプローチ

デメリット

  • 他の画面でも同じようにsubscribeしたい場合は、実装が重複する可能性があり、DRYでない

例:未完了TODOタスクに何か変更があったときに変更差分を検知したい場合

<script>
import firebase from 'firebase/app'
import 'firebase/firestore'

const db = firebase.firestore()
export default {
  mounted() {
    db.collection('todos')
      .where('completed', '==', false)
      .onSnapshot(snapshot => {
        snapshot.docChanges().forEach(change => {
	  // 変更後のデータが取得できる
	  console.log('change: ', change.doc.data())
	})
      })
  }
}
</script>

このままだと変更を監視し続けるため、永遠にFirestoreとのコネクションを維持し続けてしまいます💸
subscribeする必要がなくなれば、明示的にunsubscribeしないといけません。
Vueであれば、beforeDestroyライフサイクルで、subscribeしているコンポーネントが不要になった時(例:他の画面への遷移時など)にunsubscribeしてやればよいでしょう。

Firestoreのunsubscribe方法は、onSnapshotメソッドとは別にunsubscribeメソッド的なものが存在しないので、個人的には直感的ではないかなと思います。

方法としては、onSnapshotメソッドを関数実行するとunsubscribeすることになります。

<script>
import firebase from 'firebase/app'
import 'firebase/firestore'

const db = firebase.firestore()
export default {
  data() {
    return {
      unsubscribe: null
    }
  },
  mounted() {
    this.unsubscribe = db.collection('todos')
      .where('completed', '==', false)
      .onSnapshot(snapshot => {
        snapshot.docChanges().forEach(change => {
	  console.log('change: ', change.doc.data())
	})
      })
  },
  beforeDestroy() {
    this.unsubscribe()
  }
}

一番愚直な方法ですが、Vueのライフサイクルメソッドだけで完結するのでシンプルではあります。

Vuexのアクションで、Firestoreのsubscriptionを実装する

  • Vuexのアクションとして、Firestoreをsubscribe・unsubscribeの処理をそれぞれ追加する

TL;DR

やることは、ライフサイクルだけで完結させる最初のアプローチで実装したsubscribe実装をVuex actionに移行するだけ

メリット

  • Vuexのactionにすることで、実装を一箇所にまとめることができて、他のコンポーネントでも流用できる
  • Vuex actionなので、vue devtoolでデバッグしやすい

デメリット

  • 画面によって、subscribeした時の挙動を変えたい場合は、Vuex actionを変更すると使用しているコンポーネント全てに影響を与えてしまう。いわゆる「共通化の罠」みたいな話ですね
    • ここらへんは、デメリットというより何でもかんでもVuexに寄せればいいわけではないので、容量用法を守って良い設計していければよいと思います(投げやり)。

実装例:todo.js(Vuex module)

import firebase from 'firebase/app'
import 'firebase/firestore'

const db = firebase.firestore()

export const state = () => ({
  todos: [],
  unsubscribe: null
})

export const actions = {
  startListener() {
    this.unsubscribe = db.collection('todos')
      .where('completed', '==', false)
      .onSnapshot(snapshot => {
        snapshot.docChanges().forEach(change => {
	  console.log('change: ', change.doc.data())
	})
      })
  },
  stopListener() {
    this.unsubscribe()
  }
}

Vueコンポーネント内で、subscribe/unsubscribeのVuex actionをmount/unmount時に実行する

<script>
import { mapActions } from 'vuex'

export default {
  mounted() {
    this.startListener()
  },
  beforeDestroy() {
    this.stopListener()
  },
  methods: {
    ...mapActions('todo', 'startListener', 'stopListener')
  }
}
</script>

vuefire(ライブラリ)を使用する

このライブラリを使うメリットは、Vueアプリケーションで、Firestoreのsubscribe機能を使ったリアルタイム処理をしたい時は、以下の実装だけで済んでしまうことです😳

例:未完了のTODO一覧を取得しつつ、変更があればVue側も自動更新

<script>
export default {
  data() {
    todos: []
  },
  firestore: {
    todos: db.collection('todos').where('completed', '==', false)
  }
}
</script>

これだけでCollectionごとTODOのデータを全取得しつつ、ライブラリ側でsubscribe/unsubscribe処理をしてくれます。かなり便利!

正確には、以下のようにFirebaseのイニシャライザ処理とvuefireのfirestorePluginをVue本体に適用させる必要があります。

Nuxt.jsであれば、plugin内でfirebaseとvuefireの設定をするのが良いかと思います。

import firebase from 'firebase/app'
import 'firebase/firestore'
import Vue from 'vue'
import { firestorePlugin } from 'vuefire'

if (!firebase.apps.length) {
  firebase.initializeApp({
    apiKey: process.env.API_KEY,
    authDomain: process.env.AUTH_DOMAIN,
    databaseURL: process.env.DATABASE_URL,
    projectId: process.env.PROJECT_ID,
    storageBucket: process.env.STORAGE_BUCKET,
    messagingSenderId: process.env.MESSAGING_SENDER_ID,
    appId: process.env.APP_ID
  })
}

Vue.use(firestorePlugin)

export const db = firebase.firestore()
const { Timestamp, GeoPoint } = firebase.firestore
export { Timestamp, GeoPoint }

db.settings({ timestampsInSnapshots: true })

ライブラリに完全お任せではなく、自前でsubscribeさせるタイミングをコントロールしたい場合も、this.$bindthis.$unbindメソッドなるものがあり、エッジケースにも対応できる柔軟さを持っている良くできたライブラリだと思います。

最後に

  • vuefire使うことにしました✨

Discussion