20
11

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 3 years have passed since last update.

Firebase JS SDK v9 へのアップデート備忘録

Last updated at Posted at 2021-08-29

とあるプロジェクトの dependabot の firebase の PR がコケてました。

Cannot find module 'firebase' or its corresponding type declarations.

と思ったらいつのまにか v9 が出ていて、調べてみたら書き方がえらい変わっていたので備忘録を書きます。

公式の移行ガイド:https://firebase.google.com/docs/web/modular-upgrade

v9 の利点

Modular といわれる使い方をすれば「必要なものだけ読み込む」ことができて、アプリのサイズを減らすことができるようです(Tree Shaking)。

段階的な変更

v9 を導入したからといって全部コードを書き直さないといけないかというとそういうわけでもなく、

  1. import 文の部分だけ変える
  2. v8 の書き方と v9 の Modular の書き方を共存させる
  3. v9 の Modular に完全移行

の3つのオプションが状況に応じてとれるようです。

1. import 文の部分だけ変える

公式の移行ガイドにもありますが

import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/firestore';

import firebase from 'firebase/compat/app';
import 'firebase/compat/auth';
import 'firebase/compat/firestore';

に変えればとりあえずは動くらしいです。Moduler に対して Compat (Compatibleの略)という書かれ方をしています。時間がない人はこちらを。

3. v9 の Modular に完全移行

便宜上、先に 3. を。Modular の書き方は公式の移行ガイドによると

import firebase from "firebase/compat/app";
import "firebase/compat/auth";

const auth = firebase.auth();
auth.onAuthStateChanged(user => { 
  // Check for user status
});

import { getAuth, onAuthStateChanged } from "firebase/auth";

const auth = getAuth(firebaseApp);
onAuthStateChanged(auth, user => {
  // Check for user status
});

に書き直して、とあります。

  • firebase.auth() -> getAuth(firebaseApp)
  • auth.onAuthStateChanged(user => {}) -> onAuthStateChanged(auth, user => {})

ドット・チェーンで呼び出していたメソッドをすべて、それぞれのモジュールから直で import して書き直すのを地道にやっていけばいいです。

2. v8 の書き方(Compat)と v9 の Modular の書き方を共存させる

共存も可能で、例えば v8 に依存しているライブラリを使っている場合、そのライブラリには Compat のモジュールを渡して、それ以外では Modular を渡すなどできます。

色々省略していますが、私は以下のように使っています。

import firebase from 'firebase/compat/app'
import 'firebase/compat/firestore'
import { getFirestore } from 'firebase/firestore'

const config = {
  apiKey: ...
}

const getApp = () => (firebase.apps && firebase.apps[0]) || firebase.initializeApp(config)

getApp()

const firestore = getFirestore(getApp())
const oldFirestore = firebase.firestore()

export { firestore, oldFirestore }

firestore は Modular の書き方をして、 oldFirestore は v8 にしか対応していないライブラリに渡します。

共存のときに注意しないといけないのは、全部 Modular にしない場合は初期化コードは Compat のものを使わないといけないようです。

compat/共存
import firebase from "firebase/compat/app"

firebase.initializeApp({ /* config */ });
modularのみ
import { initializeApp } from "firebase/app"

const firebaseApp = initializeApp({ /* config */ });

変更したところ・注意点

実際に中身をどう変更したか、基本 firestore.doc('posts/1').get() -> getDoc(doc(firestore, 'posts/1')) とか単純な変換なので、つまずいたところ・気になったところだけ。TypeScript 使用の場合です。

変更前
import firebase from 'firebase'

const hoge = (timestamp: firebase.firestore.Timestamp) => {}
変更後
import { Timestamp } from 'firebase/firestore'

const hoge = (timestamp: Timestamp) => {}

みたいに直で型を import できるようになった気がします。

Functions

変更前
const deleteUser = functions.httpsCallable('deleteUser')
変更後
import { httpsCallable } from 'firebase/functions'

const deleteUser = httpsCallable<{ email: string }, { userId: string }>(
  functions,
  'deleteUser'
)
httpsCallable<{ パラメーターの型 }, { レスポンスの型 }>()

のように型を指定できるようになった気がします、v8から出来てたらすみません。他にも型が強化されてるらしいです

Firestore

自動生成IDを先に作るやつ

変更前
const postRef = firestore.collection(POSTS).doc()
変更後
import { collection, doc } from 'firebase/firestore'

const postRef = doc(collection(firestore, POSTS))

FieldValue

変更前
const postRef = firestore.doc("posts/1")
postRef.update({
  hage: firebase.firestore.FieldValue.arrayUnion('foo'),
  hoge: firebase.firestore.FieldValue.delete()
})
変更後
import { doc, updateDoc, arrayUnion, deleteField } from 'firebase/firestore'

const postRef = doc(firestore, "posts/1"))
updateDoc(postRef, {
  hage: arrayUnion('foo'),
  hoge: deleteField()
})

予約語衝突回避のために delete()deleteField() になっていたり、若干の変更がありました。

exists

変更前
firestore.doc('posts/1').get()
  .then((snap: DocumentSnapshot) => {
    if (snap.exists) {
    }
  })
変更後
import { doc, getDoc } from 'firebase/firestore'

getDoc(doc(firestore, 'posts/1'))
  .then((snap: DocumentSnapshot) => {
    if (snap.exists()) {
    }
  })

地味に exists が関数になってました。

batch

変更前
const batch = firestore.batch()
batch.set(postRef, { hoge: "foo" })
batch.commit()
変更後
import { writeBatch } from 'firebase/firestore'

const batch = writeBatch(firestore)
batch.set(postRef, { hoge: "foo" })
batch.commit()

クエリ

公式にも例が出てましたが。

変更前
firestore
  .collection('posts')
  .where('created', '<', endsWith.toDate())
  .orderBy('created', 'desc')
  .limit(20)
  .get()
変更後
import {
  query,
  orderBy,
  limit,
  where,
  collection,
  getDocs,
} from 'firebase/firestore'

getDocs(
  query(
    collection(firestore, 'posts'),
    where('created', '<', endsWith.toDate()),
    orderBy('created', 'desc'),
    limit(20)
  )
)

Auth

ソーシャルログイン

実際に使ってるコードは違いますが、同じメールアドレスで、Google でログインしていたユーザーが Twitter でログインしたみたいなときの処理の例。結構色々変えないとでした。

変更前
auth.signInWithPopup(provider)
  .then((result) => {
    // 変更なし
  })
  .catch(function(error) {
  if (error.code === 'auth/account-exists-with-different-credential') {
    const pendingCred = error.credential;
    const email = error.email;
    auth.fetchSignInMethodsForEmail(email).then(function(methods) {
      const provider = getProviderForProviderId(methods[0]); // これは自前実装
      auth.signInWithPopup(provider).then(function(result) {
        result.user.linkAndRetrieveDataWithCredential(pendingCred).then(function(usercred) {
          goToApp(); // これは自前実装
        });
      });
    });
  }
});
変更後
import { getAuth, signInWithPopup, GoogleAuthProvider, fetchSignInMethodsForEmail, linkWithCredential } from "firebase/auth";

const auth = getAuth();

signInWithPopup(auth, provider)
  .then((result) => {
    // 変更なし
  }).catch((error) => {
    if (error.code === 'auth/account-exists-with-different-credential') {
      const pendingCred = GoogleAuthProvider.credentialFromError(error)
      const email = error.customData?.email as string
      fetchSignInMethodsForEmail(auth, email).then(function(methods) {
        const provider = getProviderForProviderId(methods[0]); // これは自前実装
        signInWithPopup(auth, provider).then(function(result) {
          linkWithCredential(result.user, pendingCred).then(function(usercred) {
            goToApp(); // これは自前実装
          });
        });
      });
    }
  });

クレデンシャルの取得、メールアドレスの場所、リンクするメソッドが主に変わってました。

ログインのコールバック

変更前
auth.signInAnonymously().then((user) => {
  console.log(user.uid)
})
変更後
import { signInAnonymously } from "firebase/auth";

signInAnonymously(auth).then((userCredential) => {
  console.log(userCredential.user.uid)
})

地味に user を挟まないといけなくなってました。

Storage

変更前
task = storage.ref().child(`hoge.jpg`).put(file)

task.on(
      'state_changed',
      () => {},
      (e) => {
        console.log(e)
      },
      () => {
        task.snapshot.ref.getDownloadURL().then((url) => {
          console.log(url)
        })
      }
  )
変更後
import {
  getDownloadURL,
  ref,
  uploadBytes,
} from 'firebase/storage'

uploadBytes(ref(storage, `hoge.jpg`), file)
  .then((result) => {
     getDownloadURL(result.ref).then((url) => {
       console.log(url)
     })
  })

基本が UploadTask ではなく Promise を返すようになったようです。

uploadBytesResumable を使えば、従来通り UploadTask が返ってくるくるようです。 uploadStringResumable はないようですが...

20
11
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
20
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?