Firebase is a backend-as-a-service acquired by Google in 2014. Firebase has a buttload of features, from user authentication, app and website hosting, or serverless cloud functions. Firestore is Firebase's "database-as-a-service", which uses the document and collection paradigm of data storage. It is a NoSQL flavoured database with it's own DSL of which we will scratch the surface.
I won't get into the merits of choosing a cloud server versus hosting your own. The point of this post is to show you how easy it is to implement Firebase and Firestore into a React app.
First thing, we need to create a project using the Firebase developer console. Head over to https://console.firebase.google.com.
Give your project a name. Analytics aren't necessary, but it's up to you.
Register an app and give it a nickname. You will not require hosting for this sandbox.
Under "Build", in the sidebar, click on "Firestore Database", then click "Create database". Start the database in "test mode". You will be prompted to choose the region where your storage bucket will live.
Once your database is ready, click on "Start collection". You will be prompted to give your collection a name. In our case we will call it "todos".
To save our collection, we need to create a document. Give the todo the field "title" with a type "string" and set its "value" to whatever you like.
Now click the cog next to "Project Overview" to access "Project settings". You will find the configuration object with all the necessary keys needed to access our Firestore from our code.
Now we are set up to use Firebase and Firestore in our React app. From the root of our React app, install Firebase.
npm install firebase
Create a file for the Firebase configuration. Make sure to store the keys of your configuration as environment variables inside an .env
file, and forget to add the file to .gitignore
😉. If you are using Create React App you need to prepend your environment variable names with REACT_APP_
.
import { initializeApp } from 'firebase/app';import { collection, getFirestore } from 'firebase/firestore';
const firebaseConfig = { apiKey: process.env.REACT_APP_API_KEY, authDomain: process.env.REACT_APP_AUTH_DOMAIN, projectId: process.env.REACT_APP_PROJECT_ID, storageBucket: process.env.REACT_APP_STORAGE_BUCKET, messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID, appId: process.env.REACT_APP_APP_ID,};
initializeApp(firebaseConfig);
const db = getFirestore();const colRef = collection(db, 'todos');
export { colRef, db };
initializeApp()
creates a FirebaseApp instance and takes our firebaseConfig
object as a parameter. Next we create an instance of a Firestore database with getFirestore()
. Then we create a variable with a reference to a collection by passing the Firestore instance and the collection name to the collection()
function. Export the collection reference and Firestore instance so we can access them later on.
In App.js
we will import onSnapshot()
from the Firestore library, colRef
and our db
instance from the configuration file.
import { onSnapshot } from 'firebase/firestore';import { colRef, db } from './firebase-config';
function App() { const [todos, setTodos] = useState([]);
useEffect(() => { const unsubCol = onSnapshot(colRef, snapshot => { let todos = []; snapshot.docs.forEach(doc => { todos.push({ ...doc.data(), id: doc.id }); }); setTodos(todos); }); return () => unsubCol(); }, []);
// ...}
onSnapshot()
is a function that listens for changes in a collection. When there is a change to a document in a collection we reference, onSnapshot()
will update the collection in automagically.
onSnapshot()
takes a query, or reference to a collection, to listen to, as its first parameter. The callback then returns a document snapshot object. The snapshot contains a docs
property, which is the actual collection. We iterate over docs
and push each document into an array we will use to set the state. The doc.data()
method contains all of the document's properties, except for the ID which is a top-level property of doc
. Once that's complete we setState()
with the array.
To avoid memory leaks useEffect()
typically has a cleanup function that is executed before the next render. onSnapshot()
returns an unsubscribe function, unsubCol()
. Return unsubCol()
from the useEffect()
to detach the listener.
import { useState } from 'react';import { addDoc } from 'firebase/firestore';import { colRef } from '../firebase-config';
const Form = () => { const [title, setTitle] = useState('');
const handleSubmit = async e => { e.preventDefault();
const todo = { title, complete: false, };
try { await addDoc(colRef, todo); setTitle(''); } catch (err) { console.log('An error occurred: ', err.message); } };
// ...};
To add a document to our collection we need the addDoc()
function from the Firestore library, and the reference to our collection.
We have a simple form with a single controlled input that sets the state for the title. handleSubmit()
when invoked, creates a todo object with properties. Then we pass the todo object to addDoc()
as the second argument, with colRef
being the first. We need handleSubmit()
to be asynchronus so we can reset the title input to be a blank string after the addDoc()
Promise resolves.
onSnapShot()
in our App component detects the change and updates our collection to reflect the latest data.
import { onSnapshot, doc, updateDoc } from 'firebase/firestore';import { colRef, db } from './firebase-config';
function App() { // ...
const updateTodo = async todo => { try { const docRef = await doc(db, 'todos', todo.id); updateDoc(docRef, todo); } catch (err) { console.log('An error occurred: ', err.message); } };
// ...}
doc()
is a Firestore method that returns a reference instance to a document in our collection. It takes the Firestore instance, the collection name, and document ID as parameters.
updateDoc()
updates properties in a document referred to by a document reference. The first argument is the docRef
, while the second argument is an object that contains the properties we want updated. updateDoc()
returns a Promise, however since onSnapShot()
will update our collection in our App component, there is no need to await updateDoc()
at this time. We only need to await our document reference before we are able to pass it to updateDoc()
.
import { onSnapshot, doc, updateDoc, deleteDoc } from 'firebase/firestore';import { colRef, db } from './firebase-config';
function App() { // ...
const deleteTodo = async id => { try { const docRef = await doc(db, 'todos', id); deleteDoc(docRef); } catch (err) { console.log('An error occurred: ', err.message); } };
// ...}
deleteDoc()
deletes a document referred to by a document reference. For both our update and delete operations, the onSnapShot()
listener will handle updating our collection for us. We don't need to manually set the state of our todos.
Finally, we need to set a security rule for our Firestore database. From the official documents:
Cloud Firestore Security Rules allow you to control access to documents and collections in your database. The flexible rules syntax allows you to create rules that match anything, from all writes to the entire database to operations on a specific document.
Click on the "Rules" tab of your database, and you'll find a place to edit your security rules. This rule permits anyone read/write access to my todos databse. It expires on my daughter's first birthday, by which time I hope myself or someone else has completed my list of todos 🎉.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if
request.time < timestamp.date(2023, 7, 7);
}
}
}
The full project code can be found here.