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 :)
CodePudding user response:
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, 'default')]
}
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 functionality. Function getFirestore()
will get you a Firebase instance you need to work on, same as getApp()
. And enableIndexedDbPersistence(getFirebase())
so you will cache data locally in client browser. This will reduce DB queries if you use for example onSnapshot()
listener. Or use getDocFromCache()
combined with getDoc()
.
CodePudding user response:
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)
}