Todoist is one of the most popular to-do-list applications in the market today. It came out way back in 2007 and currently has over 25 million users worldwide. Today, we will take a deep dive into Firebase Collections and will use the Todoist Clone repository as a reference point. Do note that this is not going to focus on the React side of things, and assumes you have a basic understanding of custom hooks, state management, etc. Here’s a brief of what we’re going to cover today.
- Set up the Todoist clone in your system
- Integrate Firebase with the application
- Firestore Basics - Queries, documents, and collections
- Why Firestore?
We’re not going to bore you further with our introduction. Let’s dive straight in.
This article was originally posted at: https://quod.ai/post/how-to-use-firestore-a-deep-dive-into-todoist-clone
Set up the Todoist clone in your system
Building a clone of an app such as Todoist from scratch would mean long hours of setup. We want to help you get the right stuff, the best possible way and for that, we already have a GitHub repository with the code required. You can download the source code from here:
https://github.com/karlhadwen/todoist
Clone this code to your system using HTTPS or SSH:
https://github.com/karlhadwen/todoist.git git@github.com:karlhadwen/todoist.git
Once you clone the code, fire up your favorite code editor, ours is Visual Studio Code. Run the following command to download all the dependencies mentioned in the package.json file. You can use either yarn or npm to do this. Since we have npm that comes along with the node executable, we will be using that. After executing the command, you should see something similar to this.
To run a React application using npm, we can use the npm start command and the application will be set up at localhost:3000. You can also use the yarn start command to do the exact same thing. If you run the application right now, you will be greeted with an error message saying that ‘firebase’ is missing. To rectify this, we need to integrate Firebase with our application. But before we do that, let’s just take a look at the folder structure to understand what we’re dealing with.
├── __tests__ - Contains the test files
├── components - Houses all the components present within the application
├── constants - Constants, store some of the constants that we need within the application
├── context - Context is used to store the programming context
├── helpers - Helpers contain all of the helper functions that we will commonly use
├── hooks - This folder contains the custom React Hooks
├── App.js
├── App.scss
├── index.js
We have replaced App.css with App.scss. Do note that several files that come up when we create a basic React app have been removed. This includes App.test.js, index.css, serviceWorker.js, logo.svg, App.css, logo192.png, logo512.png, favicon.ico, manifest.json, and robots.txt. We will be focusing on the Firebase elements in this application, so let’s get started.
Integrate Firebase with the application
Integrating Firebase with our web application is extremely simple and straightforward. You can use your Google account to log in to Firebase. Then, head over to the Firebase Console and create a new project.
This will open up a project creation wizard that has just two easy steps. The first step is to enter a project name.
Once you enter the name and hit enter, you will be asked whether you want to use Google Analytics for your project. We can disable this for now.
If you disable Google Analytics, you can directly create the project after step 2. Otherwise, you will have one more step to go through. Hit the ‘Create Project’ button now and you’re all set to go.
You will be greeted with this page that says the project has been successfully created. Once you hit Continue, you will be at the project dashboard. The next step is to add Firebase to your project.
Click on the ‘</>’ button that indicates a web application. The following screen will give you all the configuration information that is required. For us, we just need to copy the 6 lines of code marked below.
To integrate Firebase into our Todoist application, we will need to create a file named firebase.js inside the src folder. This config file is then imported into all the other components as and when Firebase is required in the application.
import firebase from 'firebase/app';
import 'firebase/firestore';
const firebaseConfig = firebase.initializeApp({
apiKey: "$YOUR_API_KEY_HERE",
authDomain: "$YOUR_AUTH_DOMAIN",
projectId: "$YOUR_PROJECT_ID",
storageBucket: "$YOUR_STORAGE_BUCKET",
messagingSenderId: "$YOUR_MESSAGE_SENDER_ID",
appId: "$YOUR_APP_ID"
});
export { firebaseConfig as firebase };
Line 1-2: These are the two imports that we need. One is for Firebase and the next is for Firestore.
Line 4: We create a constant named firebaseConfig. Here we call the initialize app function on the firebase object that we imported earlier and pass in the configuration information.
Line 5-10: This is where you paste the 6 lines that we copied from the Firebase Console.
Line 13: We then import the firebaseConfig constant as firebase, so that we can use it everywhere in the project.
With that, we have successfully integrated Firebase within our application. To use Firestore in our application, we simply import this file and call the firestore() method. This will initialize an instance of Firestore with the configuration information that we provided. This instance will be used to query the db, retrieve data and make updates. We will look deeper into this aspect of Firestore in the next section and go through queries, documents, and collections.
Firestore Basics: Queries, documents, and collections
The great thing about the Todoist clone project is that even though it is feature-rich and looks complex, the underlying functionality can be easily implemented. Firestore is Firebase's NoSQL database that allows us to store information from our application like users, tasks, projects, etc. Other NoSQL databases in the market include MongoDB, CouchDB, Cassandra, Amazon DynamoDB and HBase.
Todoist Clone: Database Model
Here is a snapshot from the Firebase Console. For the todoist clone, we just have two entities in the database, projects and tasks. Tasks contain five fields: archived, data, projectId, task and userId. The archived field is used to denote whether a task has been completed or not. The data field is used to set the task date. The projectId is used to link the task to a project (it can be empty also, if there’s no project associated with the task). The task field, denotes the name of the task and the userId, denotes the user who created the task.
Projects contain three fields: name, projectId and userId. The name denotes the project name, projectId is the unique identifier that we generate using a helper function and the userId denotes the user who created the task.
Firestore simplifies a lot of the operations that are usually cumbersome with traditional databases. It allows the frontend client app to make calls directly to the DB (no backend servers needed) and that is what we’re going to capitalize on. Most traditional database clients require a middleware to process the calls from the frontend. Firestore/FBRD allows you to directly query the database from the frontend app without having to have a Node/Express, Spring, Flask backend. Since it is a NoSQL database, meaning there is no rigid structure. It stores data in the form of JSON. Simple data is stored as documents and a series of related documents are stored as collections. Collections will have multiple documents present within them, with some kind of unique id linking each document to a collection. In the screenshot above, we can see that we have two collections: projects and tasks. Each of them have multiple documents present inside them referencing the collection using an id field. Instead of having multilevel JSONs to represent complex relationships, Firestore represents them as single-level JSONs with a unique ID linking them to other documents.
Firestore is also highly optimized to have a lot of small documents in it and it allows us to listen to changes. This means that any change in the database is immediately reflected back to all the listeners. This makes it a perfect solution for a lot of real-time applications like reservation systems, online ticket booking, etc (where the chances of double booking are high). Imagine a scenario, where you’re trying to book a particular seat to a cinema on an online booking platform, and see that the seat turns gray because someone else books it right before you do. Firebase Realtime Database is another alternative that Firebase provides. But it is slightly more difficult to use and has less functionality when there is large amount of data to handle.Firebase Realtime Database (FBRD) stores data as a single large JSON file while Firestore provides better structure with bifurcation into documents and collections. Due to the indexing present in Firestore, querying will always be fast. This is because it depends only on the query result and not on the size of the dataset. Firestore also has a better billing method because it bills you based on the number of operations and not on the network bandwidth and storage.
Cloud Firestore - Basic Querying
Queries are used in Firestore to access the data and for all the create/read/update/delete operations. It is important to know how these are constructed to get a better understanding of how data comes in and goes out of the application.
import firebase from 'firebase/app';
firebase.firestore()
firebase.firestore().collection(‘subjects’)
firebase.firestore().collection(‘subjects’).doc('123')
firebase.firestore().collection(‘subjects’).where(‘subjectName’, '==', ‘Mathematics’)
Line 1: This is the import statement that is needed to use firebase within our app.
Line 2: This creates a Firestore instance with the config information that we have provided.
Line 3: This is how we reference a collection.
Line 4: This is how we reference a particular document in the collection.
Line 5: Last line shows how we can query a document that satisfies a particular condition.
The last line has three parts. ‘subjectName’, '==', ‘Mathematics’. This is how every query is constructed. It has the field which we are referring to, the relational operation (‘<’, ‘<=’, ‘>’, ‘>=’, ‘==’), and lastly the value to compare. We can also chain queries using multiple where() blocks but do note that we are not allowed to use range operators on different fields while chaining. The end product of this line of code is a ref and it doesn't actually execute the queries. Refs are references to documents or collections. They can be DocumentReference or a CollectionReference. To get the contents, we need to call the get() method at the end that returns a promise.
Cloud Firestore - Document/Query Snapshots
const subject = await firebase.firestore().collection(‘subjects’).doc('123').get();
Line 1: This line returns a promise, but we still need to resolve it to get the data. This is known as the DocumentSnapshot (in case of documents) or a CollectionSnapshot (in case of collections).
You might think that the const variable will now store the key-value pairs that are present in a particular document. But it actually returns a DocumentSnapshot. This object stores certain metadata of the document along with the data present itself. So, we need to access this by calling the data() method on the snapshot returned.
const snapshot = await firebase.firestore().collection(‘subjects’).doc('123').get();
const data = snapshot.data();
Line 1: We store the return value of the get() method into a constant called snapshot.
Line 2: We use the data() method from the snapshot to get the actual data.
In case we query for something that returns multiple documents, we don't get a DocumentSnapshot, but a QuerySnapshot. We need to access the docs property on the QuerySnapshot to get the array of documents and then use something like an arrow function to map it to a constant, calling the data() method on each document. Snapshots allow us to listen for database changes. For instance, if you see a reservation for a seat in a cinema hall, and if another person books it, you will see it turning to gray in real-time showing that it is unavailable.
Another advantage with Firestore is that documents are automatically indexed. This means that even if there’s an increase in the size of documents to be fetched the time taken to retrieve them doesn’t linearly increase.
Let’s take a look at another code snippet from the Todoist clone that explains how we can retrieve the tasks from the to-do list.
export const useProjects = () => {
const [projects, setProjects] = useState([]);
useEffect(() => {
firebase
.firestore()
.collection('projects')
.where('userId', '==', 'jlIFXIwyAL3tzHMtzRbw')
.orderBy('projectId')
.get()
.then(snapshot => {
const allProjects = snapshot.docs.map(project => ({
...project.data(),
docId: project.id,
}));
if (JSON.stringify(allProjects) !== JSON.stringify(projects)) {
setProjects(allProjects);
}
});
}, [projects]);
return { projects, setProjects };
View index.js in context at Quod AI
Line 1: This is a custom React Hook that we are using to get the list of the projects.
Line 2: This contains the project's state as well as the setProjects used to update the state if there’s a change. We use the useState() hook to make it a stateful component.
Line 4-10: This is where the actual magic happens. We use the useEffect() hook to allow our component to use lifecycle hooks in React, which was earlier only possible for class components. We then use a query to retrieve all the projects for the specific userId and order them by their project ids.
Line 11-23: As we said earlier, when we use the get() method, we get a QuerySnapshot, and we iterate through all of them using an arrow operator and store the data into the constant allProjects. It is interesting to note that we compare it with the existing list of projects and change state only if there’s any difference between the two. Finally, we return the array of projects and the setProjects method.
Cloud Firestore - Create/Update/Delete Operations
Similar to the query we saw in the example above, we can also create, update or delete in the exact same way. All of these return promises, and we can use async/await along with these. We will look at code snippets where we can create a new project, delete a project as well as update a task.
const addProject = () =>
projectName &&
firebase
.firestore()
.collection('projects')
.add({
projectId,
name: projectName,
userId: 'jlIFXIwyAL3tzHMtzRbw',
})
.then(() => {
setProjects([...projects]);
setProjectName('');
setShow(false);
});
View AddProject.js in context at Quod AI
Line 1-15: This is the snippet used to add a new project. We reference the collection using, firebase().firestore().collection(‘projects’) and then call the add() method to add a project to the collection. The projectId is generated using a function and is a random set of characters and numbers. You can take a look at src/helpers/index.js for the function (generatePushId). The project name is what we type into the text box and the userId is a predefined string to identify the user. You can use any predefined string here, but make sure to use the same string throughout.
const archiveTask = () => {
firebase.firestore().collection('tasks').doc(id).update({
archived: true,
});
};
View Checkbox.js in context at Quod AI
Line 1-5: This is a very short snippet to showcase the update functionality of Firestore. Similar to the create operation, we reference the collection using firebase().firestore().collection(), passing in the collection name. But since we need to update a specific document, we need to pass the document id, which is passed as an argument to the doc() method. To update, we simply call the update() method on this document, and set the property that we want to the new value. In this case, the archived field is set to true. On setting archived to true, the item on the todo list is removed indicating that it has been already completed.
const deleteProject = (docId) => {
firebase
.firestore()
.collection('projects')
.doc(docId)
.delete()
.then(() => {
setProjects([...projects]);
setSelectedProject('INBOX');
});
};
View IndividualProject.js in context at Quod AI
Line 1-5: We delete a project by passing in the document ID associated with the project, each time we click on the delete button. This is then used along with the query to select the particular document to delete. Similar to the retrieval, we call the firebase object’s firestore() method, reference the collection, projects, then pass in the document ID to the doc() method.
Line 6-11: Then we call the delete() method which returns a promise and we resolve it by setting the projects array with the new one and set the selected project as ‘INBOX’, which will show the Inbox view to the user.
Checkout all the firebase functions in context in Quod AI’s firebase collection for todoist.
Why Firestore?
The main reason to use Firestore is the fact that anyone without previous experience with Firebase can get up and running in no time. The fact that we were able to integrate Firebase into our web application, hook up the database and start querying it in very little time is a testament to how easy it is to use. Also, Firebase has come a long way since its inception and is perfect for hobby projects as well as commercial applications. Their billing is very flexible and is a great entry point for your next big application.
Phew! That was a long one. But we really hope that you now have a better understanding of Firestore and how you can integrate it with your web application. We saw how easy it is to use Firestore to store, retrieve and perform operations on our application data. The fact that everything happens in real-time makes it all the more exciting!
Checkout todoist source code: https://github.com/karlhadwen/todoist
Want to easily and quickly onboard and contribute to todoist? Checkout todoist on Quod AI.
Quod AI is code search and navigation on steroids. We turn code into documentation that developers actually use. Check our app free at: beta.quod.ai