Nuxt With Firebase V9 Modular (Tree Shake)

April 12, 2022

Seems like @nuxtjs/firebase is not going to support firebase v9 modular syntax, and there might not be support for firebase v10.

So the best way forward is to use Firebase directly without depending on a nuxt plugin.

Install Firebase.

npm install firebase

Edit nuxt.config.js

  • load firebaseClient on client side only
const config = {
  plugins: [
    { src: '~/plugins/firebaseClient', mode: 'client' },
  ]
}

export default config

Create plugins/firebaseClient.js

  • load firebase and analytics by default
  • load auth, firestore, etc. dynamically when required
import { initializeApp, getApps } from "firebase/app"
import { getAnalytics } from "firebase/analytics";

const firebaseConfig = {
  apiKey: "AIza******",
  authDomain: "*****.firebaseapp.com",
  projectId: "*****",
  storageBucket: "*****.appspot.com",
  messagingSenderId: "9613*****",
  appId: "1:9613*****:web:5ec9*****",
  measurementId: "G-C7*****"
}

const firebaseApp = initializeApp(firebaseConfig);

// only load analytics at production
let analytics = null
if (process.env.NODE_ENV == 'production') {
  analytics = getAnalytics(firebaseApp);
}

export default async ({ app, store }, inject) => {
  const fireModule = {
    firestore: { }
  }

  const fire = {
    app,
    analytics,
    // dynamic load auth when required
    async getApp() {
      return this.app
    },
    async getAuth() {
      if (!this.auth) {
        const { getAuth, onAuthStateChanged } = await import("firebase/auth")

        const firebaseApp = await this.getApp()
        this.auth = getAuth(firebaseApp)

        onAuthStateChanged(this.auth, async (authUser) => {
          const claims = authUser ? (await authUser.getIdTokenResult(true)).claims : null
          if (authUser) {
            // I only run app check when user signin
            await fire.getAppCheck()
          }

          // perform your action here
          // await store.dispatch('onAuthStateChangedAction', { authUser, claims })
        })
      }

      return this.auth
    },
    async getFirestore() {
      if (!this.firestore) {
        // switch to firebase/firestore/lite if onSnapshot is not used for the entire application
        const { getFirestore, doc, collection, getDoc, Timestamp, GeoPoint, deleteField } = await import("firebase/firestore")

        // simulate similar syntax with @nuxtjs/firebase
        fireModule.firestore.Timestamp = Timestamp
        fireModule.firestore.GeoPoint = GeoPoint
        fireModule.firestore.FieldValue = {
          delete: deleteField
        }

        const firebaseApp = await this.getApp()
        const db = getFirestore(firebaseApp, {})
        db.getItem = async (collectionName, docId) => {
          const docSnap = await getDoc(doc(collection(db, collectionName), docId))
          if (docSnap.exists()) {
            const item = docSnap.data()
            item.id = docSnap.id
            return item
          }
        }
        this.firestore = db
      }
      return this.firestore
    },
    async getFunctions() {
      if (!this.functions) {
        const { getFunctions } = await import('firebase/functions')

        const firebaseApp = await this.getApp()
        this.functions = getFunctions(firebaseApp)
      }

      return this.functions
    },
    async getAppCheck() {
      if (!this.appCheck) {
        const { initializeAppCheck, ReCaptchaV3Provider } = await import("firebase/app-check");

        const firebaseApp = await this.getApp()
        this.appCheck = initializeAppCheck(firebaseApp, {
          // site key
          provider: new ReCaptchaV3Provider('6Lel******'),
          // Optional argument. If true, the SDK automatically refreshes App Check
          // tokens as needed.
          isTokenAutoRefreshEnabled: true
        });
      }
      return this.appCheck
    },
  }

  // simulate similar syntax with @nuxtjs/firebase
  inject('fire', fire)
  inject('fireModule', fireModule)
}

Usage in page or component.

<script>
import { collection, doc, query, where, limit, getDocs, setDoc } from "firebase/firestore";

export default {
  props: ['id'],
  async mounted() {
    const id = this.id

    // since we are loading dynamically, await this.firestore() syntax is required
    const db = await this.$fire.getFirestore()
    // else, you could preload/assign fire.firestore in plugins/firebaseClient.js
    // const db = this.$fire.firestore

    // load single document using helper function
    const place = await db.getItem('place', id)

    // query
    const snapshot = await getDocs(query(collection(db, 'place'), where('parent_ids', 'array-contains', id), where('active', '==', true), limit(9)))
    const places = await snapshot.docs.map(doc => {
      const item = doc.data()
      item.id = doc.id
      return item
    })

    // update with merge
    let ref = doc(collection(db, 'place'), id)
    const data = {
      modified: this.$fireModule.firestore.Timestamp.now(),
    }
    await setDoc(ref, data, { merge: true })
  }
}
</script>

NOTE: You can load firebase and analytics dynamically as well.

NOTE: JavaScript Dynamic Import with Tree Shaking (via async).

References:

This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.