The latest version of the Firebase library introduces one of the most important updates in recent years, making it easier for developers to build high-quality apps with Firebase.

The new Firebase version 9 release (launched in August 2021) features a new modular API that enables tree-shaking, bundle size reduction, and other benefits.

This new module-first format is optimized for the elimination of unused code.

The result is a potentially significant reduction of Firebase library code in JavaScript bundles, up to 80% in some scenarios.

Having said that, users of Firebase version 8 will need to update their code for it to still work with version 9.0.0 and higher.

While there's a Firebase compat package to make the migration easier, that's a temporary fix.

The update not only involves the Firebase JavaScript SDK, but it also impacts the companion libraries like @angular/fire, RxFire, and, in the case of Ionic apps, the Firebase Capacitor plugin.

When I decided to migrate from Firebase 8 to version 9, I couldn't find all the resources in one place. That's why I decided to write about these changes, so you can easily follow them and keep your Ionic mobile app up to date.

Upgrading to Firebase 9: Breaking Changes and API updates

Beyond the structural changes I mentioned before, the latest Firebase v9 library comes with some breaking changes regarding its API.

As a consequence, libraries that were built upon the Firebase JavaScript SDK also changed their APIs.

Among these libraries are the @angular/fire package, the RxFire package, and the Capacitor Firebase plugin.

This means that the effort needed to upgrade to the latest Firebase library is focused on adjusting the implementation of these auxiliary packages in your codebase.

Many users were asking us to update the template to the latest version of the Firebase library (v9).

As we mentioned before, the fully modularized Firebase version 9 has these advantages over earlier versions:

  • Firebase v9 enables a dramatically reduced app size. It adopts the modern JavaScript Module format, allowing for tree shaking practices in which developers can selectively import only the Firebase components they need in their app, rather than importing the entire library.
    Depending on your app, tree-shaking with Firebase version 9 can result in 80% fewer kilobytes than a comparable app built using version 8.

  • Version 9 will continue to benefit from ongoing feature development, while version 8 will be frozen at a point in the future.

If you don't have the time or resources to invest in the full upgrade process, you can use the new Firebase compat libraries.

This way you can update to the Firebase v9 install without adjusting your implementation to comply with the internal Firebase API changes.

This is something I don't specifically recommend, but it may be a good starting point if you want a quick solution or hotfix.

One of the most important things regarding the new version of Firebase for Ionic apps in particular is to choose a well-suited Capacitor Firebase Plugin that supports the latest Firebase v9 release.

Capacitor plugins are a key part of the Ionic mobile apps ecosystem because it's the bridge between the different platforms, those being iOS, Android, and the Web.

In particular, a good Capacitor Firebase plugin would be the piece that interacts with the native Firebase SDKs (both iOS and Android) and the JavaScript Firebase SDK (for the web and Progressive Web Apps). This way all the Firebase functionalities and APIs would be exposed through that Capacitor plugin.

Here at IonicThemes, we were using baumblatt/capacitor-firebase-auth Capacitor plugin for our templates and client projects, but unfortunately it was getting behind in terms of support. Most notably, it was not compatible with the latest version of Firebase v9 and it was causing build errors on both iOS and Android when users updated their package.json dependencies of their Ionic app projects.

To get rid of these issues and take advantage of the new modular Firebase library, we decided to change the underlying Capacitor Firebase plugin we were using.

The capawesome-team/capacitor-firebase Capacitor plugin (maintained by Robin Genz) is a solid alternative. It supports Android, iOS, and the Web, and it's built on top of Capacitor 3+ and Firebase Web SDK v9. It has detailed documentation, and on top of that the maintainers are actively updating, improving and fixing bugs in the library.

Upgrading to Firebase 9: Updating @angular/fire library

@angular/fire is the official Angular library for Firebase.

AngularFire smooths over the rough edges an Angular developer might encounter when implementing the framework-agnostic Firebase JS SDK, and aims to provide a more natural developer experience by conforming to Angular conventions.

As we mentioned before, the underlying changes to the Firebase JavaScript SDK presented on the latest v9 propagated to the libraries built on top of the web SDK. Thus, we need to update the @angular/fire library and adjust some methods of the implementation.

Firebase reactive programming with RxFire

RxFire is a utility library that uses RxJs operators to interact with the underlying Firebase library. It enables an easy async Firebase for all frameworks.

I'm a big fan of RxJs and reactive programming in general, so this is my preferred approach to interact with the different Firebase products such as Cloud Firestore.

The RxFire library is not something we use directly in our codebase (although you can). In this case some modules of the @angular/fire library include, make use and export the RxFire utilities.

Our Ionic 6 Full Starter App template includes examples and integrations of many Firebase features such as Cloud Firestore, Realtime Database, and Firebase authentication.

With our templates, you will be able to access advanced CRUD examples with Firestore and the ability to query collections.

The template also includes a complete authentication flow built on top of Firebase that allows you to authenticate users using email/password and social providers, ensuring personalized experiences across all devices and securely saving user data in the cloud.

Ionic Firebase CRUD integration
Ionic Firebase authentication integration
Ionic Firebase Twitter authentication integration

Our templates in combination with Firebase allow you to build mobile apps quickly without the need to manage infrastructure, saving you time and effort.

Overall, this template provides powerful tools and easy-to-use features like Firestore to help you build robust and scalable apps.

To benefit from the Firebase v9 features and improvements, we need to install the latest version of the library.

npm install --save firebase@9

After updating Firebase to v9 we should also update the @angular/fire library as this is the piece of code we are gonna use to indirectly interact with the Firebase JavaScript SDK. This step also updates the RxFire functionality used by the @angular/fire library under the hood.

ng update @angular/fire --create-commits

Once we update these libraries, we can start adjusting the Firebase implementation in our codebase.

We had to update many files in the template because we did not just update to Firebase 9, we also updated to a new Capacitor Firebase plugin and the latest version of the @angular/fire library as well.

Cloud Firestore offers a flexible NoSQL cloud database that can scale to meet the needs of the storing and syncing features of your app in both client-side and server-side implementations.

Like Firebase Realtime Database, it keeps your data in sync across client apps through real time listeners while also offering offline support for mobile and web so you can build apps that work regardless of network latency or Internet connectivity.

Cloud Firestore also provides seamless integration with other Firebase and Google Cloud products, including Cloud Functions.

We need to adjust our code to the changes this new version of the library brings.

Initializing the @angular/fire/firestore library

There's a new way to initialize the library:

import { getFirestore, provideFirestore } from '@angular/fire/firestore';
import { initializeApp, provideFirebaseApp } from '@angular/fire/app';

…

@NgModule({
  imports: [
    …

    provideFirebaseApp(() => initializeApp(environment.firebase)),
    provideFirestore(() => getFirestore())
  ]
})
export class FirebaseCrudModule {}
Different ways to retrieve data from Firebase Cloud Firestore

There are two ways to retrieve data stored in Cloud Firestore:

  • Call a method to get the data
  • Set a listener to receive data-change events

When you set a listener, Cloud Firestore sends your listener an initial snapshot of the data, and then another snapshot each time the document changes.

@angular/fire/firestore exports the same methods you can use from Firebase 9. Here's a list of all the available methods from the source code. You can also check the official Firebase 9 documentation for more references.

Get data from a Cloud Firestore document

The following example shows how to retrieve the contents of a single document using get():

import { Firestore, doc, getDoc } from '@angular/fire/firestore';

// Should be injected in the Angular constructor
const db = Firestore;

const docRef = doc(db, "cities", "SF");
const docSnap = await getDoc(docRef);

if (docSnap.exists()) {
  console.log("Document data:", docSnap.data());
} else {
  // doc.data() will be undefined in this case
  console.log("No such document!");
}

Note: If there is no document at the location referenced by docRef, the resulting document will be empty and calling docSnap.exists() on it will return false.

Listen for data-change events on a Cloud Firestore document

An initial call using the callback you provide creates a document snapshot immediately with the current contents of the single document. Then, each time the contents change, another call updates the document snapshot.

import { Firestore, doc, onSnapshot } from '@angular/fire/firestore';

// Should be injected in the Angular constructor
const db = Firestore;

const unsub = onSnapshot(doc(db, "cities", "SF"), (doc) => {
  console.log("Current data: ", doc.data());
});

@angular/fire/firestore also exports methods from rxfire/firestore.

Here you can find a list with all the available RxFire methods from the @angular/fire/firestore source code.

You can also check the RxFire Firestore documentation for more references.

I went a step further and gathered some of the most common use cases and here I show you how to implement them using RxFire utilities.

Cloud Firestore initialization

First, in order to use the library in your Ionic Angular project, you first need to initialize it in your Angular module.

import { getFirestore, provideFirestore } from '@angular/fire/firestore';

@NgModule({
  imports: [
    …
    provideFirestore(() => getFirestore())
  ],
})
export class FirebaseCrudModule {}
Inject the Firestore service in the constructor

After that, you are safe to inject the service in any of your components.

import { Firestore } from '@angular/fire/firestore';

…

constructor(
  private afs: Firestore
) { }

…
To query documents with RxFire use docData() instead of valueChanges()

The RxFire replacement for the old Firebase v8 valueChanges() method is the new Firebase v9 docData() method.

import { Firestore, docData, doc } from '@angular/fire/firestore';

…

docData<Post>(
  doc(this.afs, 'posts', id)
);
To query documents with RxFire use docSnapshots() instead of snapShotChanges()

The RxFire replacement for the old Firebase v8 snapShotChanges() method is the new Firebase v9 docSnapshots() method.

import { Firestore, docSnapshots, doc } from '@angular/fire/firestore';

…

docSnapshots<Post>(
  doc(this.afs, `posts/${id}`)
);
To query collections with RxFire use collectionData() instead of valueChanges()
import { Firestore, collectionData, query, collection, CollectionReference, where } from '@angular/fire/firestore';

…

collectionData<Post>(
  query<Post>(
    collection(this.afs, 'posts') as CollectionReference<Post>,
    where('published', '==', true)
  ), { idField: 'id' }
);
To query collections with RxFire use collectionChanges() instead of snapShotChanges()
import { Firestore, collectionChanges, query, collection, CollectionReference, where } from '@angular/fire/firestore';

…

collectionChanges<Post>(
  query<Post>(
    collection(this.afs, 'posts') as CollectionReference<Post>,
    where('published', '==', true)
  )
);
To get a document ID use doc().id instead of createId()

This is useful if you need to generate a random ID for the doc before saving it to Firestore. It's also handy if your use case requires setting the ID as a reference field in another document, even before Firestore saves anything. This approach also speeds up the process because you don't need to wait for the ID of the document before continuing with the execution of your code.

import { Firestore, doc, collection } from '@angular/fire/firestore';

…

doc(collection(this.afs, 'id')).id;
What's the difference between get() and onSnapshot() in Cloud Firestore?

When you use get() you "retrieve the content of a single document" only once. It's a kind of "get and forget": If the document changes in the Firestore database you will need to call get() again to see the change.

On the other hand, if you use the onSnapshot() method you constantly listen to document changes pushed from the Firestore database. The snapshot handler will receive a new query snapshot every time the query results change (that is, when a document is added, removed, or modified).




What's the difference between valueChanges() and snapshotChanges() and stateChanges()?

valueChanges() returns the current state of your collection, without any snapshot metadata.

snapshotChanges() retrieves also the metadata (DocumentChangeAction). This includes among other things the DocumentChangeType ('added' | 'modified' | 'removed') and DocumentSnapshot properties like id and methods like exists(): boolean and data(): DocumentData.

stateChanges() returns everything on the initial subscription (and then, when changes occur, it subsequently returns an object with the details of the changes). This means you get all the recent and future DocumentChangeAction[] changes. This is useful if you want to show the changes history of a collection or reduce them (for example show all the changes made to a collection recently).




Why do you include the { idField: 'id' } option?

When using the valueChanges() method, we only get the document data. All snapshot metadata is stripped.

Optionally, we can pass an options object with an idField key containing a string. If provided, the returned JSON objects will include their document ID mapped to a property with the name provided by idField.

Firebase Authentication drastically simplifies the process of authenticating users to your app. It supports authentication using passwords, phone numbers, and the most popular social auth providers such as Google, Facebook and Twitter, GitHub, and more.

Using the same Firebase SDKs mentioned above, you get all these features ready to go.

In addition, if you upgrade to Firebase Authentication with Identity Platform, you unlock additional features, such as multi-factor authentication, blocking functions, user activity and audit logging, etc.

@angular/fire/auth migration procedure

This new version of @angular/fire/auth (and the underlying Firebase 9 library), requires some adjustments to the Firebase implementation as well as some method calls.

Initializing the @angular/fire/auth library

There's a new way to initialize the library:

import { provideFirebaseApp, initializeApp } from '@angular/fire/app';
import { getAuth, provideAuth } from '@angular/fire/auth';

…

@NgModule({
  imports: [
    ...

    provideFirebaseApp(() => initializeApp(environment.firebase)),
    provideAuth(() => getAuth())
  ]
})
export class FirebaseAuthModule {}

But according to this issue and this section of the Firebase documentation, we need to adjust the initialization to make sure it works properly with Ionic Capacitor.

provideAuth(() => {
  if (Capacitor.isNativePlatform()) {
    return initializeAuth(getApp(), {
      persistence: indexedDBLocalPersistence
    });
  } else {
    return getAuth();
  }
})
Using Angular auth guards with Firebase Auth

Auth guards also changed with the new Firebase v9 SDK. This is the proper way of implementing an authentication guard now:

import { canActivate, AuthPipeGenerator } from '@angular/fire/auth-guard';

// Firebase guard to redirect logged in users to profile
const redirectLoggedInToProfile: AuthPipeGenerator = (next) => map(user => {
  // When queryParams['auth-redirect'] don't redirect because we want the component to handle the redirection
  if (user !== null && !next.queryParams['auth-redirect']) {
    return ['firebase/auth/profile'];
  } else {
    return true;
  }
});

const routes: Routes = [
  {
    path: '',
    component: FirebaseSignInPage,
    ...canActivate(redirectLoggedInToProfile)
  }
];
import { redirectUnauthorizedTo, canActivate, AuthPipe } from '@angular/fire/auth-guard';

const redirectUnauthorizedToLogin: () => AuthPipe = () => redirectUnauthorizedTo(['/firebase/auth/sign-in']);

const routes: Routes = [
  {
    path: '',
    component: FirebaseProfilePage,
    resolve: {
      data: FirebaseProfileResolver
    },
    ...canActivate(redirectUnauthorizedToLogin)
  }
];

If you are looking for some more advanced authentication guards use cases, I suggest you check the source code for reference.

AngularFire lacking documentation

Unfortunately, the documentation regarding these updates has not been updated fully yet. The information is dispersed and sometimes I end up reading the source code in order to better understand how to use the new library.
Here's a list of all the available methods of this library from the source code.

Don't get me wrong here, Firebase v9 documentation is abundant, but AngularFire documentation is still lagging.

Let's first recap some differences between the new Capacitor app runtime from the Ionic team compared to Cordova and PhoneGap (some of the old pioneers regarding mobile hybrid apps).

Why is Capacitor a better alternative than Cordova/PhoneGap?

Choosing the right cross-platform development tool

Capacitor is an open-source project that runs modern Web Apps natively on iOS, Android, Electron, and Web (using Progressive Web App technologies) while providing a powerful and easy-to-use interface for accessing native SDKs and native APIs on each platform. As an alternative to Cordova, Capacitor delivers the same cross-platform benefits, but with a more modern approach to app development, taking advantage of the latest Web APIs and native platform capabilities.



In plain English, this means that for example, building a feature that accesses the native camera API of the device can be achieved with the same code on iOS/Android as it does on Electron and on the web. This enables you to easily build one web app that runs natively on mobile, desktop, and the web as a Progressive Web App.

Like Capacitor, Cordova is an open-source project that runs web apps across multiple platforms, though not Electron nor web as a Progressive Web App.

Cordova is the open-source core of the commercial Adobe PhoneGap project, and they can both be considered equivalent.

With Capacitor, native and web development teams can work side-by-side. Under the hood, Capacitor apps are actual native apps and a key design consideration of Capacitor is embracing native tooling. As such, Capacitor enables teams that have a mixture of traditional native mobile and web developers to collaborate on mobile app projects.

Cordova, in contrast, works through an abstraction layer that manages the underlying native platform project and source files for you. This makes it harder to drop down to native code or work with a traditional native mobile development process, and can result in custom changes being lost.

Moving away from old Cordova plugins

Capacitor is a bit different compared to Cordova. It doesn't compile the platform (ios, android) everytime a build is made. Capacitor compiles the platforms once, and further changes can be simply copied into the platforms using:
npx cap sync.

Plugins are installed as npm or yarn packages in Capacitor.

This simplifies the plugin management, being easier to install new Capacitor plugins and remove unused ones.

Updating to the latest Firebase Capacitor plugin

As mentioned before, one of the first Firebase Capacitor plugins that we were once using and recommending, started to get behind in terms of features, updates and maintenance.

That's why we decided to migrate to the new capawesome-team/capacitor-firebase Capacitor plugin.

Uninstall old Firebase Capacitor plugin

To remove the old Capacitor Firebase plugin, just run:
npm uninstall --save capacitor-firebase-auth.

This will remove the package from node_modules and also update the package.json. You will also need to undo specific plugin configurations on the different platforms to completely uninstall the plugin.

Install new Capacitor Firebase plugin

This is the simplest step. Just run:
npm install --save @capacitor-firebase/authentication.

Update plugin configuration

Most of the configurations are the same as the new Capacitor Firebase plugin as they use the same social auth providers native SDKs (Facebook and Google). I just did a manual diff between the two configurations in the project.

Both configurations are straightforward, just follow the steps in the plugin's documentation.

Firebase authentication on hybrid apps with Capacitor

Hybrid apps are a mix of native and web code and contexts. Most of the time these contexts are not interconnected. That's also the case for the authentication flow.

Handling Firebase authentication on hybrid apps

We can choose to only use the web authentication on our Ionic app through the Firebase JS SDK (firebase and @angular/fire packages installed using npm or yarn).

This only works to a limited extent on Android and iOS in the WebViews (see here).

That's why you should consider implementing a native authentication on your Ionic app.

If you follow this path, the native SDKs from Firebase, Google, Facebook, etc. will be used.

These offer all the functionalities that the Firebase JS SDK also offers on the web. However, after login in with the native SDK, the user is only logged in on the native layer of the app.

This means that if we plan to use the Firebase JS SDK (for example to check if the user is logged in using the @angular/fire/auth library, we must perform additional steps to transition from the native authentication to the web authentication (see here).

Luckily the @capacitor-firebase/authentication plugin helps us achieve a consistent implementation across all platforms. It uses the Firebase SDK for Java (Android), Swift (iOS) and JavaScript.

The ultimate flow to achieve top usability would be to implement the native authentication using the different social auth providers SDKs and then transfer the auth state to the Firebase JavaScript SDK which is the one we are gonna interact with within our codebase.

In this guide, I went through all the steps, procedures, updates, bug fixes, and issues I stumbled upon when updating our templates and client's projects to the latest Firebase v9.

If you are having headaches updating your codebase to the latest Firebase v9 library, or the improved Capacitor Firebase plugin, then please contact us and we can provide you with technical assistance through one of the Ionic Developer Experts that are part of our team.