🍍

PiniaのstateとFirestoreをかんたんに同期するpinia-plugin-firestore-sync

2022/01/16に公開

を作ったのでご紹介させてください!

npm
https://www.npmjs.com/package/pinia-plugin-firestore-sync

レポジトリはこちら(🌟をもらえると嬉しいです!)
https://github.com/kazuooooo/pinia-plugin-firestore-sync


モチベーション

PiniaFirestoreはどちらも開発を大幅に効率化させる素晴らしいライブラリとデータベースですが、PiniaのstateとFirestoreのドキュメントを同期させるにはonSnapShotを使って以下のように書く必要があります。


export const useExampleStore = defineStore('expamle', {
  state: {/*...*/}
  actions: {
    async setup() {
      // ...
      //🌟 docDataとdocRefを同期
      onSnapshot(docRef, (ds) => {
        if (ds.exists()) {
          this.$patch({ docData: ds.data() })
        }
      })
    }
  }
})

このonSnapshot、開発していると同じ処理を何度もいろんなところで書く必要が出てきて、DRYでは無くなってきます。

さらに1つのstore内で複数のDocumentと同期したい場合なんかは何度も縦に並べて書くことになり冗長で、見通しも非常にわるいです。

単純にPiniaのStateとFirestoreのドキュメントを同期したいだけなのに内部の詳細を知りすぎてる感じも非常に気持ち悪いですね。

async setup() {
  // 何回もonSnapshotを書く必要があり、めちゃくちゃ冗長に😱
  onSnapshot(docRefA, (ds) => {
    if (ds.exists()) {
      this.$patch({ docDataA: ds.data() })
    }
  })
  onSnapshot(docRefB, (ds) => {
    if (ds.exists()) {
      this.$patch({ docDataB: ds.data() })
    }
  })
  onSnapshot(docRefC, (ds) => {
    if (ds.exists()) {
      this.$patch({ docDataC: ds.data() })
    }
  })
}

そこでこの問題を解決するために作成したのがpinia-plugin-firestore-sync です。

このプラグインを使用すると先程のコードは非常にシンプルに書くことができるようになります!(やったね🙌)

async setup() {
  // やったね🙌
  this.sync('docDataA', docRefA)
  this.sync('docDataB', docRefB)
  this.sync('docDataC', docRefC)
}

使い方

READMEにも同様の記載がありますが、こちらでも日本語でご紹介します。

まずはプラグインをインストール

npm install pinia-plugin-firestore-sync

プラグインをuseを使って追加します。

import { PiniaFirestoreSync } from 'pinia-plugin-firestore-sync'

// プラグインを追加
const pinia = createPinia().use(firestoreSyncPlugin)
app.use(pinia).mount('#app')

ドキュメントと同期する場合

同期させたいPiniaのプロパティ名を第一引数、Document Referenceを第二引数に渡すことで簡単に同期ができます。

this.sync(
  'docData', // Document Dataと同期させたいpiniaのプロパティ名
   docRef    // Document Reference
)
サンプルコード
import { doc, getFirestore } from "firebase/firestore"
import { defineStore } from "pinia"

type ExampleDoc = {
  name: string,
  age: number
}

export type State = {
  docData: ExampleDoc | null,
}

export const useExampleStore = defineStore('expamle', {
  state: (): State => {
    return {
      docData: null,
    }
  },
  actions: {
    async setup() {
      // Get Document reference
      const store = getFirestore()
      const docRef = doc(store, 'Examples/id')

      // Do the magic
      this.sync('docData', docRef)
    }
  }
})

コレクションと同期する場合

コレクションとも同期させることができます。

こちらも同期させたいPiniaのプロパティ名を第一引数、Collection Referenceを第二引数に渡すだけです。簡単ですね!

this.sync(
  'collectionData' // Collection Dataと同期させたいpiniaのプロパティ名
   collectionRef   // Collection Reference
)
サンプルコード
import { collection, getFirestore } from "firebase/firestore"
import { defineStore } from "pinia"

type ExampleDoc = {
  name: string,
  age: number
}

export type State = {
  collectionData: ExampleDoc[] | null,
}
export const useExampleStore = defineStore('expamle', {
  state: (): State => {
    return {
      collectionData: null,
    }
  },
  actions: {
    async setup() {
      // Get Collection reference
      const store = getFirestore()
      const collectionRef = collection(store, 'Examples')

      // Do the magic
      this.sync('collectionData', collectionRef)
    }
  }
})

クエリと同期する場合

クエリとも同期させることができます。

もうわかってるとは思いますが、同期させたいPiniaのプロパティ名を第一引数、Collection Referenceを第二引数に渡すだけです。予想通りですね笑

this.sync(
  'queryData' // Collection Dataと同期させたいpiniaのプロパティ名
   query      // Query
)
サンプルコード
import { collection, getFirestore, query, where } from "firebase/firestore"
import { defineStore } from "pinia"

type ExampleDoc = {
  name: string,
  age: number
}
export type State = {
  queryData: ExampleDoc[] | null,
}
export const useExampleStore = defineStore('expamle', {
  state: (): State => {
    return {
      queryData: null,
    }
  },
  actions: {
    async setup() {
      // Build query
      const store = getFirestore()
      const collectionRef = collection(store, 'Examples')
      const q = query(collectionRef, where('name', '==', 'wombat'))

      // Do the magic
      this.sync('queryData', q)
    }
  }
})

内部的な仕組み

内部的な仕組みが気になった方もいるかと思うので、かんたんにご紹介。

まずPiniaでは以下のようにプラグインを書くことで簡単にstoreに共通のメソッドやプロパティを生やすことができます。

import { PiniaPluginContext } from "pinia";

// プラグイン
const magicNumPlugin = ({ store }: PiniaPluginContext) => {
  store.magicNumber = 5
}

// 型付
declare module 'pinia' {
  export interface PiniaCustomProperties {
    magicNumber: number
  }
  export interface DefineStoreOptionsBase<S, Store> {
}

その仕組みを使ってsyncを生やします。

30行程度の非常に短いコードです。

import { CollectionReference, DocumentReference, onSnapshot, Query, Unsubscribe } from "firebase/firestore";
import { PiniaPluginContext } from "pinia";

export const PiniaFirestoreSync = ({ store }: PiniaPluginContext) => {
  store.sync = (key, ref) => {
    // Document
    if (ref instanceof DocumentReference) {
      return onSnapshot(ref, (ds) => {
        if (ds.exists()) {
          store.$patch({ [key]: ds.data() })
        }
      })
    }

    // Collection or Query
    return onSnapshot(ref, (qs) => {
      const datum = qs.docs.map(d => d.data())
      store.$patch((state) => {
        state[key] = datum
      })
    })
  }
}

declare module 'pinia' {
  export interface PiniaCustomProperties<Id, S, G, A> {
    sync(key: string, ref: DocumentReference): Unsubscribe
    sync(key: string, ref: CollectionReference): Unsubscribe
    sync(key: string, ref: Query): Unsubscribe
  }
}

実装はonSnapShot同様同じメソッド名でDocument/Collection/Query全てを渡せるようにしたかったので、渡された型によって内部でif分岐させています。

store.sync = (key, ref) => {
  // Document
  if (ref instanceof DocumentReference) {
    return onSnapshot(ref, (ds) => {
      if (ds.exists()) {
        store.$patch({ [key]: ds.data() })
      }
    })
  }

  // Collection or Query
  return onSnapshot(ref, (qs) => {
    const datum = qs.docs.map(d => d.data())
    store.$patch((state) => {
      state[key] = datum
    })
  })
}

そしてインターフェースで渡され得る型をOverloadの形で定義します。

declare module 'pinia' {
  export interface PiniaCustomProperties<Id, S, G, A> {
    sync(key: string, ref: DocumentReference): Unsubscribe
    sync(key: string, ref: CollectionReference): Unsubscribe
    sync(key: string, ref: Query): Unsubscribe
  }
}

以上、piniaのプラグインはまだまだ公開されているものも少ないので、皆さんもチャレンジしてみてはいかがでしょうか!

Discussion