Setup a Database in React With Firebase Cloud Firestore

Deinyefa
Deinyefa
Aug 10, 2020 · 8 min read

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.

Image for post
Image for post
Photo by Amirali Mirhashemian on Unsplash

What is Firestore?

Getting Started

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.

Image for post
Image for post
menu for menu, orders to group users

Create some burgers

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

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

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

/orders/[uuid]/order/[timestamp]
Image for post
Image for post
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.

Image for post
Image for post
add or remove items to update the cart
Image for post
Image for post
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

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.

Image for post
Image for post
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

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.

The Startup

Medium's largest active publication, followed by +752K people. Follow to join our community.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store