27
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Firebase JS SDK v9アップグレードは移行大変だけどfirestoreで死ぬほどいい事があった

Last updated at Posted at 2021-09-03

こんにちは。virapture株式会社もぐめっとです。

mogmet.jpg

cafe soultreeというところでの一枚なのですが、ドラマ撮影でも使われるくらい素敵なところなのでぜひ皆さんごはんしてみてください。

今回はtypescriptを使っているプロジェクトでFirebase js sdkのv9アップグレードをしたのですが、その時のメモや、便利関数みたいなものを紹介しようと思います。(Nuxtを使うのりで書いてはいますが、一応汎用的に使えるような内容でサンプルを置いてます。)

v9へのアップグレードのメリットについては公式に解説を譲ります。だいたいパフォーマンスよくなるってかんじです。

それではやってみましょう!

v9にアップグレードする

まずはupgradeします。ncuいれてないひとはnpm install -g npm-check-updatesでいれておきましょう。

$ ncu -u firebase
$ npm i

package.jsonをみて、9以上になってればおkです。

初期化準備

dotenv使ってenvから必要なapiKeyとか呼び出していますが、必要に応じて書き換えちゃってください。

~/plugins/firebase.ts
import { FirebaseApp, getApps, initializeApp } from 'firebase/app'
import { getFirestore } from 'firebase/firestore'

const firebaseApp = getFirebaseApp()
export const firestore = getFirestore(firebaseApp)

function getFirebaseApp(): FirebaseApp {
  const apps = getApps()
  if (apps.length > 0) {
    return apps[0]
  }
  return 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,
    measurementId: process.env.MEASUREMENT_ID
  })
}

Firestoreの書き換え例

新しい書き方はgetDoc, getDocs, updateDoc, setDoc, deleteDocといった関数を使う形の使い方になりました。
before / afterで紹介していきます。

collection add

before

import { firestore } from '~/plugins/firebase'

await firestore.collection('posts').add({title: 'タイトル'})

after

import { addDoc, collection } from 'firebase/firestore'
import { firestore } from '~/plugins/firebase'

await addDoc(collection(firestore, 'posts'), {title: 'タイトル'})

collection read

before

import { firestore } from '~/plugins/firebase'

const snapshot = await firestore.collection('posts').get()
const posts = snapshot.docs.map(doc => doc.data())

after

import { getDocs, collection } from 'firebase/firestore'
import { firestore } from '~/plugins/firebase'

const snapshot = await getDocs(collection(firestore, 'posts'))
const posts = snapshot.docs.map(doc => doc.data())

document read

before

import { firestore } from '~/plugins/firebase'

const snapshot = await firestore.doc('posts/1').get()

after

import { getDoc, doc, collection } from 'firebase/firestore'
import { firestore } from '~/plugins/firebase'

const snapshot = await getDoc(doc(firestore, 'posts/1'))

また、CollectionReferenceを定義していてそこからidを算出していた方法も記載しておきます。
before

import { firestore } from '~/plugins/firebase'

const postsCollection = firestore.collection('posts')
await postsCollection.doc('1').get()

after

import { getDoc, doc, collection } from 'firebase/firestore'
import { firestore } from '~/plugins/firebase'

const postsCollection = collection(firestore, 'posts')
await getDoc(doc(postsCollection, '1'))

document set / update

before

import { firestore } from '~/plugins/firebase'

await firestore.doc('posts/1').set({title: 'Cameconはフォトコンサイト'}, { merge: true })
await firestore.doc('posts/1').update({title: 'Cameconはフォトコンサイト'})

after

import { setDoc } from 'firebase/firestore'

await setDoc(doc(firestore, 'posts/1'), {title: 'Cameconはフォトコンサイト'}, { merge: true })
await updateDoc(doc(firestore, 'posts/1'), {title: 'Cameconはフォトコンサイト'})

条件によって追加されるQuery

値があるときだけqueryを追加したいケースがあると思うのですが、その場合の書き方はこちらになります。

before

async function fetchPosts(startDate?: Date) {
    let query = this.getPostsCollection()
      .where('startDate', '<=', new Date())
      .orderBy('startDate', 'desc')
      .limit(10)
    query = startDate ? query.startAfter(startDate) : query
    const snapshot = await query.get()
...
}

after

async function fetchPosts(startDate?: Date) {
    let execQuery = query(
      this.getPostsCollection(),
      where('startDate', '<=', new Date()),
      orderBy('startDate', 'desc'),
      limit(10)
    )
    execQuery = startDate
      ? query(execQuery, startAfter(startDate))
      : execQuery
    const snapshot = await getDocs(execQuery)
...
}

query関数はQueryの型であればなんでも突っ込めるみたいなので入れ子状に再度query関数で入れ直すことによって実現してます。

Listenする(onSnapshot)

firestoreの醍醐味であるリアルタイムにデータが降ってくるonSnapshotも少し書き方がかわりました。

before

function subscribePost(): () => void {
  return firestore.doc('posts/1').onSnapshot(
    snapshot => {
      snapshot.data()
      ...
    }
  )
}

after

import { onSnapshot, Unsubscribe } from 'firebase/firestore'

function subscribePost(postId: string): Unsubscribe {
  return onSnapshot(doc(firestore, 'posts/1'), 
    (snapshot) => {
        snapshot.data()
        ...
    }
  )
}

地味に帰り値の型が新たに定義されてますね。

死ぬほどクソ便利な型補完

この記事でも言及されてますが、DocumentReferenceやらCollectionReferenceに型も一緒に定義してあげるとなんと、もれなく型チェックしてくれます!何これ便利やん!!!!!

例えばIPostというinterfaceがあった場合の書き方を紹介します。

before

await firestore.doc('posts/1').update({title: 0}) //何も言われない

after

await updateDoc(doc(firestore, 'posts/1') as DocumentReference<IPost>, {
  title: 0 // おこられる
})

コンパイルの時点で怒ってくれるので型が変わったとしてもちゃんと追従してくれるし、addのときとかは必要要素がないとaddできないとか怒ってくれるのでこれによって圧倒的に保守性がアップします。
感動しました。

ただ、注意点としてtypeとinterfaceが入り混じった複雑なものを型として定義してるとエラーになることがあったので、型はなるべくどちらかに統一して使ったほうがよさそうです。

ちなみに、readのときもdocument.data()の帰り値が自動的に指定した型になるのでいちいちdocument.data() as IPostとかかかなくてもすむようになります。

便利関数としてまとめてみた

毎回firestoreを入れるのも手間なので、firestore呼ばずにすむように便利関数を定義してみました。

~/plugins/firebase.ts
import { FirebaseApp, getApps, initializeApp } from 'firebase/app'
import {
  doc,
  collection,
  getFirestore,
  CollectionReference,
  DocumentReference,
  WriteBatch,
  writeBatch,
  collectionGroup,
  Query
} from 'firebase/firestore'

const firebaseApp = getFirebaseApp()
function getFirebaseApp(): FirebaseApp {
  const apps = getApps()
  if (apps.length > 0) {
    return apps[0]
  }
  return 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,
    measurementId: process.env.MEASUREMENT_ID
  })
}

const firestore = getFirestore(firebaseApp) // 基本はexportせず、下記関数を通してfirestoreを使う

export function getCollection<T>(path: string): CollectionReference<T> {
  return collection(firestore, path) as CollectionReference<T>
}

export function getBatch(): WriteBatch {
  return writeBatch(firestore)
}

export function getCollectionGroup<T>(collectionId: string): Query<T> {
  return collectionGroup(firestore, collectionId) as Query<T>
}

export function getDocument<T>(path: string): DocumentReference<T> {
  return doc(firestore, path) as DocumentReference<T>
}

これ一つ作っておけば少し書くのが楽ちんになります。

example.ts
import { getDoc, addDoc, updateDoc } from 'firebase/firestore'
import { getDocument, getCollection } from '~/plugins/firebase'

async function sample() {
  // add
  await addDoc(getCollection<IPost>('posts'), { title: 'Camecon始めよう!' })
  // read
  const document = getDocument<IPost>('posts/1')
  const snapshot = await getDoc(document)
  // update
  await updateDoc(document, { title: 'https://camecon.me' })
}

まとめ

正直量が多いと移行めちゃめちゃ大変でした。なので早めに移行しましょう!
移行すればサイズが減ってパフォーマンスがあがったりだとか、補完きくようになるなど地味にいいことがあるのでやっとくと良いと思います!

今回紹介できなかった他のサンプルについてはこちらの記事がよりよくまとまっているので参考にすると良いと思います。

また、今回色んな所でfirestoreを読んでいるがゆえに移行が大変だったという経緯もあるので、なるべくfirestoreの呼び出し(だけに限らずfunctionsやauthもそうだけど)はRepository層とかにまとめておくと今後の移行コストや保守コストも下がっていくんじゃないかなと思います。

27
22
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?