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

こんにちは。[virapture株式会社](https://virapture.com)の[もぐめっと](https://mogmet.com)です。

![mogmet.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42234/b4dc93c2-7599-f7bf-5dbd-ea91b2bbb683.jpeg)

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

https://cafesoultree.jp/

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

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

https://firebase.google.com/docs/web/modular-upgrade#benefits

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

## v9にアップグレードする

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

```bash
$ ncu -u firebase
$ npm i
```

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

## 初期化準備

[dotenv](https://www.npmjs.com/package/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

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

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

after

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

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

### collection read

before

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

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

after

```ts
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

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

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

after

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

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

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

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

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

after

```ts
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

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

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

after

```ts
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

```ts
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

```ts
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

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

after

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

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

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

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

https://zenn.dev/arark/articles/60927323db8697

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

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

before

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

after

```ts
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' })
}
```

## まとめ

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

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

https://qiita.com/kiyopikko/items/f1da74f4f921ea778a61

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