What I Learned at Work this Week: Firebase

Mike Diaz
10 min readMar 14, 2021
Photo by Dương Nhân from Pexels

This week we’ve got another “The Knowledge House made me do it” entry. I had a meeting with a student who was interested in using Firebase to store data for her mobile app and she asked if I had an opinion. I did not, but I vowed to form one because I prefer being able to answer questions that people ask me! So…

What is Firebase?

Firebase can do a lot of things. It’s pitched as an all-in-one product that can handle data storage, auth, analytics, error reporting, user engagement, and more. Since I was speaking with a student who was specifically asking about data storage, I decided to explore that product. With Firebase, we can quickly build a noSQL database through a combination of code and UI. Let’s give it a shot.

Starting a Firebase Project

Selecting “Get Started” from the Firebase homepage will prompt you to name and register an app. It was much easier than I expected, so I had already finished registering before I realized (there were two questions: what is your project name? and do you want to use Google Analytics?). As we can see here, I’ve already registered a project:

With that finished, you’ll have the opportunity to access the Firebase SDK (software development kit) to add Firebase to a new or existing codebase. We’ll go back to my trusty React testing repo to see how easily we can make this work with React. Here are the SDK instructions I received:

I don’t know if that data is sensitive or not, but better safe than sorry!

The instructions start with something I’ve often read at the start of an SDK, and something that I have always struggled with: “paste these scripts into the bottom of your <body> tag…” As a Bootcamp grad, I’ve pretty much always used React to render HTML in my apps. The create react app command does generate an index.html file with a <body> in it, but it’s not a common practice to add scripts there. That’s because React is already using a bundler to collect and combine scripts into one file — our bundle.js file. We’ll take a look at it shortly. In the meantime, we can seamlessly add Firebase to our project with an npm (or yarn) command:

npm install firebase --save

As we learned in a previous blog post, we can find details about our bundler in package.json. After installing firebase, we can see that it’s been added to our dependencies object:

The code behind firebase has been added to node_modules under @firebase. If you open it up, you’ll see that there are a ton of files there, because Firebase can do a ton of different things! Though we’ve added all this Firebase to our project, our app itself still doesn’t have any reference to it. Want proof? Run npm start and open up devtools, then check the Elements tab. My particular project is running three scripts, though all come from the same folder: static.

These are the bundled files that contain all the dependencies that we’ve imported into our project. We haven’t imported Firebase yet, so it’s nowhere to be found. Navigate over to the Sources tab in devtools and check out the contents of these files. A quick cmd + f will reveal zero instances of firebase:

This is all a long way to say that, instead of manually adding the script tag like the Firebase docs suggest, we’re going to directly import the Firebase files from our node_modules into our scripts bundle. So let’s import, save, refresh our app page, and check again:

That’s a lotta Firebase

Wow that’s a big difference! I think we can safely say we’ve now got the Firebase script running on our app. The next step in the setup instructions translates directly. We have to define a firebaseConfig and run the firebase initializeApp function before rendering our DOM:

Eagle-eyed readers may have noticed that the Firebase documentation places initializeApp directly after the firebase import object, but I had to first reference firebase.default before initializeApp. According to this thread on GitHub, this has to do with an importing quirk and webpack semantics. I’m not sure why this affected me but not the instructor in the video I was following, but nonetheless there are several workarounds, including the very simple solution of using the object as its been defined by our import: firebase.default gives us access to all the functionality we’ll need.

Cloud Firestore

I originally heard about Firebase as a storage solution, so we’ll see if we can get that working now that we’ve imported the library. If you want to follow along at home, navigate over to the Cloud Firestore section of your Firebase console (you’ll likely already be in the console if you’ve just set up your project like me). There’s another setup flow, which is just as easy as what we did to create our first project. I chose the default for my DB location and Test mode for my security rules since this is just…a test.

Looking at the docs, we’re then instructed to add Firebase to our web app. Didn’t we just do that? Yes, actually, we did. In this case we’re told to add two scripts and two imports instead of just one. The good news for us is that our npm command installed the contents of both of these scripts and a whole lot more. And our import statement used the wildcard * to import every Firestore package. This isn’t necessarily ideal for the size of our bundle, but for testing purposes it makes our lives easier.

As we continue to read, we see that we’ve already written initializeApp, but we haven’t declared a variable using firebase.firestore(). Let’s do that next:

var db = firebase.firestore()

I once again had to use firebase.default, but the function still worked. As you might imagine, a lot is being abstracted in this one line. The firestore function creates a database-like object with a ton of built-in methods that give us tremendous customization power. To start, we’ll look to do what we’d normally do with a database: write a schema and add some data!

There’s a bunch of code here, so let’s break it down. Our db object contains a collections function, which takes a string argument. A Firebase collection is akin to a table in a traditional database. The argument we provide is the name of our collection, which we will use to reference it later.

The collections function has another function, add, chained to it, which is used to add a document to our collection. A document is like a table row, so we add a lego set to our table.

But wait — you might be asking — didn’t you say we were going to write a schema and then add data? We did! Firestore makes this intuitive by simultaneously creating the collection and adding the document at the same time. If we give the collections function an argument that we haven’t used before, it will create a new collection. A schema isn’t necessary, as documents don’t necessarily have to all share the same properties, unlike rows in a traditional SQL table.

After we’ve provided the add function with data about the classic Lunar Launch Site Lego set, we see a .then. That means that the result of add is a promise. No surprise there since there’s bound to be potential latency when we’re communicating between a frontend and a backend, even if it is as slick as Firestore. Our .then function is simply a confirmation that the document has been written. We call the object returned by and’s promise docRef and check its id property. As with traditional databases, a unique ID is assigned to our documents upon creation. Finally, we see a catch statement in case something goes wrong with add.

There are a lot of things that I find difficult about databases, but one that comes up most frequently is being able to see how they’re structured and easily review their data. The Cloud Firestore console provides a neat UI where you can immediately confirm that your code worked. Keep in mind that I had to save my index.js file and then manually refresh my React app page to get the new code to run:

And all without a single SQL command!

Referencing our Cloud Firestore Data

Now that we’ve added a document to our collection, we can access the information with a get function:

db.collection(“lego_sets”).get()

Like add, get will return a promise based on the value we’ve provided collection. In this case, we’re pulling all of the documents from our lego_sets collection. The sample code from the docs led me to believe that the promise would return an array:

db.collection(“users”).get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(`${doc.id} => ${doc.data()}`);
});
});

Following this assumption, I wrote code that would save the entire querySnapshot in a state Hook and then map through that Hook to render my data on a page. But I ended up with an error: legoSets.map is not a function. I had used Hooks to create a state called legoSets and set it as an array by default. I then simply replaced the state with querySnapshot:

db.collection(“lego_sets”).get().then((querySnapshot) => {
setLegoSets(querySnapshot);
});

I thought maybe this was a classic React error where I was replacing state instead of updating it with a spread operator, but this somehow made things even worse:

db.collection(“lego_sets”).get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
console.log(`${doc.id} => ${doc.data()}`)
setLegoSets([…legoSets, doc.data()]);
});
});

This caused an infinite loop that crashed my app, even when I added a useEffect with a constant or empty dependency. Based on my research, the issue here was that each time we ran setState, the app re-rendered, which caused our db.collection().get() logic to run again, which updated the state again and caused another re-render. This wasn’t an issue for me in the past because fetch doesn’t work the same way that get does. It didn’t take long to find a way to fix this using more Firebase, but I wanted to see if there was a way to actually do this with Hooks, and indeed there was:

The big change we see in this function, inspired by Ben McMahen, is that we’re pushing elements into a static array and then using the setState Hook to save the final array as our state. This saves us from running setState within a loop. By passing the final state into components, we get the data on our page!

useCollectionData

The code I’ve ended up with is bulky and hard to read, and it took me a couple of hours to figure out how to get it right. Continuing with the theme that Firebase and Firestore are packed with features, it comes as no surprise that this logic can be abstracted by the built-in function useCollectionData. Here’s the refactor:

Wow that’s a lot less code! First I specifically imported the useCollectionData function from the react-firebase-hooks directory in firestore (I think this was already installed but if not the command to get it is npm i react-firebase-hooks). Then all the action happens when I destructure the result of running useCollectionData with two arguments. The first is the query itself, which asks for our collection of legoSets. The second argument is an object of optional customizations. In this case, we used idField, which just takes in the collection ID.

The function returns an array with three elements. The first is the values pulled from the query and the other two, which we didn’t use here, are a boolean to indicate if the data is loading and a query error, if one exists.

Scratching the Surface

The world of Firebase is vast, as illustrated that the variety of documents, blog posts, and video tutorials that their team has put out. When there’s so much to learn, it’s tempting to just follow the instructions and ignore what might be going on under the hood. It’s always beneficial to slow down while learning and try to understand the abstractions so that we can use the framework more effectively. We either learn it now, or we learn it later, while we’re trying to debug an error that we don’t understand.

Sources

--

--