firebase • firestore • javascript
Hello, Firestore: Adding Live Data to Your Web Apps
by Steve Marx on
Firebase Cloud Firestore is a document-oriented database that provides clients with realtime updates.
I recently used its cousin, Firebase Realtime Database, to build thislink.works. Firestore is quite similar, and I get the impression it’s destined to be a replacement.
This post will be the first of several posts about Firestore, so I’ll start with the basics: how to add Firestore to a web app, how to write data, and how to listen for changes to that data.
For this post, I’ll be focusing on web apps, but don’t be confused: Firestore (and the rest of Firebase) is fully cross-platform.
Toggle With Friends
The example app I built is called “Toggle With Friends”, and you can play with it at toggle-3333.web.app or right here. Just click the box to toggle it between black and white. If someone else (or you in a different browser) does the same thing, you’ll both see the box update live:
Now that I’ve blown your mind with what’s possible, let’s build this app step by step.
Setting up a Firebase project
To use Firestore, you first need a Firebase project. You can create one in the Firebase console in your browser, but I find it easier to do it all from the command-line tool. You’ll want the tool no matter what to run your project locally and deploy it to the cloud.
Installing the command-line tool
You can install the Firebase tools in a number of ways, but if you have npm
installed, npm install -g firebase-tools
will do the trick. Then run firebase login
to log in using your browser.
Creating a project
Create a new directory for your project and run firebase init
. This will ask you a number of questions. First you’ll be prompted to choose a set of Firebase services you want to use. I recommend picking “Firestore”, “Hosting”, and “Emulators”. That combination lets you do everything locally. Don’t worry if your website will be hosted elsewhere. I’ll cover that later in this post.
Once you’ve chosen your features, the tool will ask you if you want to use an existing project or create a new one. To create a new one, you’ll need to provide a globally unique project ID. (I chose toggle-3333
.)
At this point, you’ll see an error message like this:
=== Firestore Setup
Error: It looks like you haven't used Cloud Firestore in this project before.
Go to https://console.firebase.google.com/project/<your-project-ID-here>/firestore
to create your Cloud Firestore database.
Go ahead and do that, and choose “start in test mode”. This gives you permissive security rules, but I’ll show you how to lock those down at the end of this post.
Rerun firebase init
, and this time use the existing project you just created. For most of the remaining questions, you can just pick the defaults. When asked which emulators to use, choose “Firestore” and “Hosting”.
Running things locally
The emulators let you run everything on your local computer. They can be started with firebase emulators:start
. Once the emulators are running, you can browse to http://localhost:5000
to see the default page that was created for you. You can edit that page in public/index.html
, unless you picked a different location during project initialization.
Deploying
At any time, you can use firebase deploy
to deploy everything to the web.
Adding the Firebase SDK(s) to your web app
If you followed the steps above, your index.html
will have this code in it already, along with some other stuff you don’t actually need. Feel free to delete liberally. All you need to make use of Firestore is the following:
<script defer src="/__/firebase/8.2.1/firebase-app.js"></script>
<script defer src="/__/firebase/8.2.1/firebase-firestore.js"></script>
<script defer src="/__/firebase/init.js?useEmulator=true"></script>
Note that this code is specific to using Firebase Hosting because it makes use of Firebase’s “reserved Hosting URLs”. In particular, that last <script>
pulls in your project’s ID, API key, and other settings automatically.
If you don’t want to use Firebase Hosting, you need to do things just a little more manually. Firebase’s documentation covers the options.
Initializing the database
Because the script tags are deferred, be sure to wait until the page has finished loading before you call firebase.firestore()
to initialize the database:
document.addEventListener('DOMContentLoaded', () => {
const db = firebase.firestore();
});
Firestore data model
The Firestore data model is a hierarchy of documents. A document is a lot like a JSON object. It’s a map of key/value pairs. Values can be scalars, lists, or maps, potentially leading to a nested structure. Each document lives in a collection, which is a named container for documents. It also acts as a namespace, so a document ID (name) must be unique within its parent collection. Collection existence is implicit. If there is at least one document belonging to a given collection, then the collection can be said to “exist”.
Finally, documents can contain subcollections, which in turn contain more documents, which can in turn contain more subcollections, ad infinitum. You might wonder why you need subcollections if your documents can contain lists of other objects directly. There are a few reasons I won’t get into now, but subcollections can help with query performance and granularity.
Phew! After all that, our “Toggle With Friends” example just has a single collection, called “boxes”, with a single document, called “the_box”. You can get a reference to it like so:
const doc = db.doc("boxes/the_box");
You may prefer this equivalent:
const doc = db.collection("boxes")
.doc("the_box");
Writing data
With a document reference in hand, you can call .set()
to write to it. Pass in a JavaScript object with your key/value pairs. This overwrites any data currently stored in the document. Use .update()
instead if you want to only change certain fields. I chose .set()
here so it will work even if the document doesn’t yet exist:
const box = document.getElementById('box');
let black = false;
box.addEventListener("click", (e) => {
doc.set({
black: !black,
});
})
The document we’re updating with each click has a single field called black
with a boolean value.
Listening for data changes
Using the same document reference, you can listen for changes to the document using .onSnapshot()
. Pass in a callback, which will be invoked each time the document changes. Note that when the document doesn’t exist, snapshot.data()
will be undefined
:
const box = document.getElementById("box");
doc.onSnapshot((snapshot) => {
if (snapshot.data() !== undefined) {
black = snapshot.data().black;
}
box.style.backgroundColor = black ? "black" : "white";
});
You can use .get()
instead if you want to perform a query just once instead of listening for future changes.
Although we don’t need it for this example, it’s also possible to listen for changes with more complex queries. For example, you could query for all documents in a collection where the field public
is true
. Any time the results of that query changed, you would receive a callback.
A quick note about security
Firestore’s security rules are a topic for another post, but it’s important to understand the basics so you don’t make expensive mistakes. All access to data in Firestore is gated by a set of rules, which you author. If the requested operation is allowed by any of those rules, it will succeed.
If you followed along with this tutorial, you now have a firebase.rules
file that looks like this:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.time < timestamp.date(2021, 2, 2);
}
}
}
This allows arbitrary reads and writes until a given date: one month from database creation. This is great for prototyping, but you’ll almost certainly want to change this before you share your app with others.
Firestore security rules are quite flexible. Here’s a simple replacement that locks things down to a single field of a single document:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /boxes/the_box {
allow read;
allow write: if request.resource.data.keys().hasOnly(["black"])
&& request.resource.data.black is bool;
}
}
}
By default, all actions are denied, so this says to only allow access to the document the_box
in the collection boxes
. It allows read access to anyone and only allows writes that have a single field called black
with a boolean value.
Summary
- Firestore is a hierarchical document-oriented database.
- Clients can be notified in realtime when data changes.
- Data access is governed by custom security rules.
Further reading
The Cloud Firestore documentation is a good place to go if you want to dig deeper. Or subscribe here via RSS or my newsletter to see my upcoming posts about Firestore.
Full source code
The full HTML source code is below, but also note the firestore.rules
file above.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Toggle With Friends</title>
<script defer src="/__/firebase/8.2.1/firebase-app.js"></script>
<script defer src="/__/firebase/8.2.1/firebase-firestore.js"></script>
<script defer src="/__/firebase/init.js?useEmulator=true"></script>
<style>
#box { width: 300px; height: 300px; margin: 0 auto; cursor: pointer }
body { background-color: #ddd; margin: 12px; padding: 12px }
</style>
</head>
<body>
<div id="box"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const box = document.getElementById('box');
let black = false;
const db = firebase.firestore();
const doc = db.doc("boxes/the_box");
box.addEventListener("click", (e) => {
doc.set({
black: !black,
});
})
doc.onSnapshot((snapshot) => {
if (snapshot.data() !== undefined) {
black = snapshot.data().black;
}
box.style.backgroundColor = black ? "black" : "white";
});
});
</script>
</body>
</html>