skip to Main Content

I’m not very experienced in Firebase. Recently I’ve been doing stuff with Next.js + Firebase and stumbled upon this line of necessary code:

const app = !getApps().length ? initializeApp(config) : getApp()

From my understanding, this prevents multiple Firebase apps with the same config from being created. But, first of all, where does this come from? And second, how does the getApps() function know about all other apps that are not DEFAULT? Is its return mutable or read-only? How does that getApp() function (with no "s" in the end) even know which app is my default to return it, I don’t pass anything to it…

I could find nothing about this nor in the Firebase docs nor from their main speakers like David East, Todd Kerpelman, Frank van Puffelen. I know, Firebase docs are literally worst on the planet, Mario games’ are much better, but even then…

Help 🙂

2

Answers


  1. There is something like this in a Firebase SDK:

    const FirebaseApp: FirebaseApp[]
    
    export function initializeApp(options: FirebaseOptions, name?: string | undefined) {
        return !name ? FirebaseApp = [...FirebaseApp, new FirebaseApp(options, 'default')] : FirebaseApp = [...FirebaseApp, new FirebaseApp(options, name)]
      }
    
    export function getApps() {
      return FirebaseApp
    }
    
    export function getApp(name?: string) {
      return !name ? FirebaseApp.filter(n => n.name === 'default') : FirebaseApp.filter(n => n.name === name)
    }
    

    Firebase JS SDK is written in TypeScript.

    In your code, you don’t need const app = ... just abuse all Firebase functions. Function getFirestore() will get you a Firebase instance you need to work on, same as getApp(). And you can enableIndexedDbPersistence(getFirebase()) so you will cache data locally in client browser and no need any Redux/Pinia/Vuex solutions. This will reduce DB queries if you use for example onSnapshot() listener. Or use getDocFromCache() combined with getDoc().

    Edit:

    Well, if you forget how OOP work and start to think in functional/structural programming, it’s starts to be obvious how does it work. Your app is encapsulated, but you have a "getters" "setters" to be able to work on it. Much better and easier to understand then OOP. There are no any design patterns to learn. And the library can be easily tree shakable by compilers/bundlers, so it’s lightweight.

    Login or Signup to reply.
  2. Building on the answer by @Mises, I can provide some additional context.

    As part of the built in protections to help developers avoid mistakes and race conditions, initializeApp() will throw an error if called twice for the same application name (where not giving a name uses "[DEFAULT]" instead). It was also designed this way because it’s easier to just throw an error instead of comparing the configuration objects passed into each initializeApp() call against the previous one. Because of this behavior, initializeApp() should be called in your application just once, either at the top of the current file or in some central dependency (e.g. app.js). Then when you need it, you can bring it into the current file using getApp(), getFirestore(), and so on.

    The getApp() and getApps() functions are part of a feature of the Firebase SDKs where you can use multiple projects in one application. The use of this feature is documented here.


    Loading the Firebase Dependency

    For some developers, Firebase is quite the heavy dependency (especially with the legacy JavaScript SDK). So its understandable that they wouldn’t want to load it in unnecessarily. This is particularly important for web-based applications where time-to-interactivity is important or when trying to optimize cold-start times for Cloud Functions for Firebase for the best response times.

    In this older video on optimizing cold-start times by @doug-stevenson, Doug covered how to use a Boolean flag to indicate whether the Firebase Admin SDK was initialized or not. This allowed a function that doesn’t use the Admin SDK to skip loading it and return a result faster.

    // note: legacy syntax being used for historical purposes
    const functions = require("firebase-functions");
    
    let is_f1_initialized = false;
    
    // a HTTPS Request function that uses the Admin SDK
    exports.f1 =
    functions.https.onRequest((req, res) => {
      const admin = require("firebase-admin");
      if (!is_f1_initialized) {
        admin.initializeApp();
        is_f1_initialized = true;
      }
      
      // does stuff, using admin SDK
    });
    
    // a HTTPS Request function that doesn't use the Admin SDK
    exports.f2 =
    functions.https.onRequest((req, res) => {
      // does stuff
    });
    

    Some developers don’t like littering their global scope with such flags, so they looked for a just-in-time alternative. This took the form of checking the length of firebase.apps in the legacy JavaScript SDK and admin.apps in the Admin SDK.

    // note: this code block uses the legacy "firebase-admin" library syntax
    import * as admin from "firebase-admin";
    
    console.log(admin.apps.length); // logs '0'
    admin.initializeApp();
    console.log(admin.apps.length); // logs '1'
    

    The same approach worked in the client-side JavaScript SDK too:

    // note: this code block uses the legacy "firebase" library syntax
    import * as firebase from "firebase";
    
    console.log(firebase.apps.length); // logs '0'
    firebase.initializeApp(config);
    console.log(firebase.apps.length); // logs '1'
    

    For single-project apps, this quickly became a de-facto standard for checking if the default application was initialized, leading to the following lines turning up everywhere (especially when using one-component-per-file frameworks):

    // note: historical legacy "firebase" library syntax used on purpose
    const app = firebase.apps.length ? firebase.app() : firebase.initializeApp(config);
    // or for those against implied type coercion to Booleans:
    // const app = !firebase.apps.length ? firebase.initializeApp(config) : firebase.app();
    const db = firebase.firestore(app);
    

    or

    // note: historical legacy "firebase" library syntax used on purpose
    if (!firebase.apps.length) {
      firebase.initializeApp(config);
    }
    const db = firebase.firestore();
    

    Summary / TL:DR;

    With the move to a modular Firebase JavaScript SDK, both for "firebase" and "firebase-admin", developers and newcomers working with legacy code are updating it by following the modular SDK migration guide.

    This leads to the following legacy code:

    // note: historical legacy "firebase" library syntax used on purpose
    const app = !firebase.apps.length ? firebase.initializeApp(config) : firebase.app();
    

    being translated one-to-one to this modern code:

    const app = !getApps().length ? initializeApp(config) : getApp();
    

    The primary purpose of this line is to get a properly initialized instance of the FirebaseApp class without throwing an error, that you can pass to the entry point functions of Firebase services included in the SDKs such as Analytics and Cloud Firestore.


    A Peek Under the Hood

    To see how the default application instance is handballed between services in the SDK, you can take a look at the source code. The FirebaseApp-related functions are implemented similar to the following code.

    Note: I’ve omitted some validation and renamed some variables to keep it concise, you should check out the full source or look at the API reference for details.

    const _apps = new Map<string, FirebaseApp>();
    const DEFAULT_ENTRY_NAME = "[DEFAULT]";
    
    // initializes the given app, throwing an error when already initialized
    export function initializeApp(options: FirebaseOptions, name?: string | undefined): FirebaseApp {
      name = name || DEFAULT_ENTRY_NAME;
      if (_apps.has(name)) throw new Error("already initialized");
      const app = new FirebaseApp(options, name)
      _apps.set(name, app);
      return app;
    }
    
    // returns a read-only array of initialized apps, doesn't throw errors
    export function getApps(): FirebaseApp[] {
      return Array.from(_apps.values())
    }
    
    // gets the named/default app, throwing an error if not initialized
    export function getApp(name: string = DEFAULT_ENTRY_NAME): FirebaseApp {
      const app = _apps.get(name);
      if (!app && name === DEFAULT_ENTRY_NAME) return initializeApp();
      if (!app) throw new Error(name + " not initialized");
      return app;
    }
    
    // marks the given app unusable and frees its resources
    export async function deleteApp(app: FirebaseApp): Promise<void> {
      const name = app.name;
      if (!_apps.has(name)) return; // already deleted/started deletion?
      _apps.delete(name);
      await Promise.all(
        Object.values(app._providers)
          .map(provider => provider.release())
      )
      app.isDeleted = true;
    }
    

    Each service available in the SDK has an entry point function. In the legacy namespaced SDKs this took the form of firebase.firestore() and the modern modular SDKs use getFirestore() instead. Each of these entry point functions follow a similar strategy and look similar to the below code.

    Note: As before, this is a simplified version. See the full source and API reference for details.

    export function getFirestore(app?: FirebaseApp) {
        app = app || getApp(); // use given app or use default
        return app._providers.get('firestore') || initializeFirestore(app, DEFAULT_SETTINGS)
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search