Setup a Database in React With Firebase Cloud Firestore

Deinyefa
The Startup
Published in
8 min readAug 10, 2020

Storing and retrieving data can be a pain when working in an environment that focuses more on the “V” part of an MVC framework such as React. For most applications, I use Firebase Cloud Firestore to store and manage data because it handles the “Model” part pretty well.

In this article, I will demonstrate how I use Cloud Firestore to save and query data in a React environment. The example project I’ll be using is called Burger Builder, an application that lets you order burgers from a menu and see your previous orders. An administrator would be able to create new varieties to be added to the menu. The code for this example project is in the Burger Builder Github repo.

Photo by Amirali Mirhashemian on Unsplash

What is Firestore?

Firestore is a Firebase NoSQL database product with strong user-based security that makes storing, syncing, and querying data very easy. The user interface is also very intuitive, so it’s easy to wrap your head around how to best structure your data.

Getting Started

You must first create a new Firebase project (or use an existing one) and find your Firebase config object that would be needed in your React environment.

Note that it’s also important to set up authentication when using Firestore in your projects to limit access to your database, this will become important a little later in this article. I have outlined how to set up a new Firebase project that handles authentication in React in a previous article which you can follow.

Once your project has been set up and connected to your React application with authentication ready, you may start using Cloud Firestore by navigating your “Database” in the Firebase console. You will then be prompted to follow the database creation flow and be asked to select a starting mode for your Cloud Firestore Security Rules.

I typically start in locked mode, then modify the rules to allow anyone to read the data and only authenticated users to write to it.

rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read
allow write: if request.auth != null;
}
}
}

This will be progressively fine-tuned as we move along but this is good enough as a starting point for most projects. I tend to start in “locked mode” to avoid ever forgetting to tighten the security settings before publishing any project. I’d rather loosen security if needed than have to remember to tighten it.

The next thing I think about is how I’d like to structure the database. This is important because depending on how the application — and it’s data — grows, scalability becomes a concern. We don’t want to suffer slower data retrieval because our database was not structured properly.

Take a look at the docs to read about the different options and weigh the pros and cons of each approach.

For Burger Builder, I separated the menu from the orders, with each user grouped under the orders collection.

menu for menu, orders to group users

Create some burgers

For this project, only 3 pieces of information matter to me for each burger; the ingredients, the price, and the name of the burger. When setting up the menu, these would be all I need.

import app from "firebase/app";
import "firebase/firestore"
this.db = app.firestore();createBurger = (ingredients, name, price) =>
this.db.collection("menu").doc(name).set({
ingredients,
name,
price,
});

Instead of letting Firebase auto-set a doc id for me, I’m giving it the name of the burger. It might make it easier for me to find and update or delete burger information later on.

Note that I’m using set to push data up to Firestore; this creates a new doc in the menu collection if there isn’t one with the same name already. If the doc already exists, it would overwrite it. As stated in the docs, if you’re not sure whether the document exists, pass the option to merge the new data with any existing document to avoid overwriting entire documents.

createBurger = (ingredients, name, price) =>
this.db.collection("menu").doc(name).set({
ingredients,
name,
price,
}, { merge: true });

There’s one last thing I’d like to mention before moving on to pulling data down. I only want an administrator to be able to upload menu items. Our current Cloud Firestore Security Rules set is not good enough because it currently allows any authenticated user to write to the database.

We can change that by allowing anyone to read the menu but only the admin to write to it and allow only authenticated users to read and write to orders. This is a simple case because I only need one account to have “admin” access, so I can get away with using their uid to grant access.

rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /menu/{document=**} {
allow read
allow write: if request.auth != null && request.auth.uid == "[admin uid]"
}
match /orders/{document=**} {
allow read, write: if request.auth != null
}
}
}

Order a Burger

Since we already allow read access to the menu for anyone, we can simply make a get request on the menu document to pull down all the available menu options.

props.firebase.fetchBurgers comes from firebase.js

import app from "firebase/app";
import "firebase/firestore"
this.db = app.firestore();fetchBurgers = () => this.db.collection("menu").get();

When fetching data like this, Cloud Firestore will order the result by creation descending— the way it’s ordered in your Firebase console. You can choose how you’d like to order the data with orderBy, limit how many you want to fetch with limit and query for specific kinds of data with where.

const burgersRef = db.collection("menu");burgersRef.where("price" < 10").orderBy("price").limit(2)

One thing to note about using orderBy and where in the same query is that if you include a filter with a range comparison (<, <=, >, >=), your first ordering must be on the same field. So you cannot do something like:

burgersRef.where("price" < 10").orderBy("name").limit(2)

Place an Order

The structure for orders is a bit different; they are grouped by the currently logged in user’s UID — unique identifier. Under each user, there’s another grouping because each user could have more than one order. Each user’s order is grouped by the timestamp of that order. This type of sub-grouping is made easy with the Firestore subcollection structuring pattern.

In the end, my database structure for a user’s orders looks like this:

/orders/[uuid]/order/[timestamp]
an “other” group could also be created for users that have not yet signed up

To push data this way, we first need to know the currently signed-in user. This is easy to find because we only have one Firebase instance running.

getCurrentUser = () => this.auth.currentUser;

On the user-facing side, I implemented a very basic add-to-cart functionality where users can click an item on the menu, select a quantity, and add that to the cart. When satisfied, clicking “Place Order” will push the cart items as well as the grand total up to Firebase. The code behind this is in the Burger Builder Github repo.

add or remove items to update the cart
validate and publish to Firebase

Since we’re grouping orders by users, the Firebase call has to be reflected accordingly:

import app from "firebase/app";
import "firebase/firestore"
this.db = app.firestore();placeOrder = (order, uuid) => {
const d = app.firestore.Timestamp.fromDate(new Date()).seconds.toString();
return this.db
.collection("orders")
.doc(uuid)
.collection("order")
.doc(d) //- d needs to be a string
.set(order);
}

The component will then supply the order and uuid. On “Place Order” click:

const placeOrder = () => {
const uuid = props.firebase.getCurrentUser().uid;
const grandTotal = cart.reduce((acc, curr) => acc + curr.total, 0);
const order = { cart, grandTotal };
props.firebase.placeOrder(order, uuid)
.then(() => console.log("Sucessfully written to doc!"))
.catch((err) => console.log("Something went wrong!", err));
};

Note: array.reduce is a really cool ES6 method that executes a reducer function (that you provide) on each element of an array, resulting in single output value.

View your Order History

Fetching a user’s orders is similar to fetchBurger but with an added complexity. As stated earlier, this is because we’ve grouped a user’s orders by when they placed the order.

fetchOrders = (uuid) => this.db.collection("orders").doc(uuid).collection("order").get();

The fetch call with useEffect is also very similar to what was used to fetch the menu items. The difference in this case comes mostly in the user interface.

bare minimum

We need to update our Cloud Firestore Security Ruleset one last time. We should make it so that if another user was created and they tried to see another user’s previous orders, they’d get blocked by Firebase (it’s always to a good idea to display things in the UI prettily; no more white pages!). We only want people to be able to see their own data, not anyone else’s:

rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /menu/{document=**} {
allow read
allow write: if request.auth != null && request.auth.uid == "..."
}
match /orders/{userId}/{document=**} {
allow read, write: if request.auth != null && request.auth.uid == userId
}
}
}

The expression userId allows the variable userId to be used when setting the rules. This means that {userId} in /orders/{userId} needs to match the user’s UID.

Conclusion

This article and the related repo are intended mostly for demonstration purposes. So much more can be done to improve the project and fix potential bugs, so feel free!

One example of such is allowing users to change the quantity directly in the cart instead of them having to find the burger in the menu and changing it from there. There’s also currently no way to completely remove an item from the cart once it’s been added.

As far as Firebase and Cloud Firestore goes, this is enough to get you started and help smooth out common questions. The Firebase docs are pretty exhaustive and should have a solution to any other issues that might arise. Also, check out my Github profile for more projects like this.

--

--

Deinyefa
The Startup

UX Engineer ⋅ JavaScript ⋅ Hooked on ReactJS