PiniaのstateとFirestoreをかんたんに同期するpinia-plugin-firestore-sync
を作ったのでご紹介させてください!
npm
レポジトリはこちら(🌟をもらえると嬉しいです!)
モチベーション
PiniaとFirestoreはどちらも開発を大幅に効率化させる素晴らしいライブラリとデータベースですが、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