CRUD app with React Firebase SDK version 9

Sat Oct 16 2021

In this tutorial we will build react app to perform CRUD operations on firebase firestore using latest firebase SDK version 9. Firebase version 9 has breaking changes so we will see how to work with firestore using firebase version 9.

We will create a todo app. User will be able to add a task to do, mark it as completed or incomplete and delete the task.

Create a react app. Open terminal in your desired directory and run

npx create-react-app app-name

Move into project directory and run

npm install firebase

Open the project in code editor and create a file firebase.js in src directory and paste the following

import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  databaseURL: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: "",
};

const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

export { db };

You can grab the firebase configuration from your firebase project. There will one missing field which is databaseURL. Firebase now dont automatically create database so for value of databaseURL it will be https://your-firebase-project-name.firebaseio.com

Firebase SDK version 9 is based on modular approach so we only import functions we need. At the top we imported initializeApp from firebase/app and since we will use firestore as our database so we also imported getFirestore function from firebase/firestore.

After configuration, we initialized our app and pass configuration to it and store it in app variable. We also initialized firestore by passing the app as argument to getFirestore function and stored it into db variable. Finally we are exporting the db variable to use it in our project.

Now we will work on adding a new task. In src directory create another directory components and inside it create a file AddTask.js and paste the following code

import { useState } from "react";
import { db } from "../firebase";
import { collection, addDoc } from "firebase/firestore";

const AddTask = () => {
  const [title, setTitle] = useState("");

  const handleSubmit = async (e) => {
    e.preventDefault();
    await addDoc(collection(db, "tasks"), {
      title,
      createdAt: new Date(),
      completed: false,
    });
    setTitle("");
  };
  return (
    <form onSubmit={handleSubmit}>
      <h3>Add Task</h3>
      <div className="input_container">
        <input
          type="text"
          placeholder="Enter task todo..."
          value={title}
          onChange={(e) => setTitle(e.target.value)}
        />
      </div>
      <div className="btn_container">
        <button>Add</button>
      </div>
    </form>
  );
};

export default AddTask;

At the top we imported useState from react and db from our firebase configuration file. We also imported collection and addDoc functions from firebase/firestore.

We initialized an empty state title to store user input. In this component we only returned a form which contains an input field to allow user to enter task and a button to submit the task.

In handleSubmit function we used addDoc function from firestore, addDoc takes collection as argument which we also imported from firestore, collection takes reference to firestore, collection name and fields we want to store into firestore. So here firestore reference is db which we imported from firebase configuration file, collection name is tasks and fields are title, createdAt which is the date and completed which by default is false.

Goto App.js file and replace the existing content with the following code

import "./App.css";
import AddTask from "./components/AddTask";

const App = () => {
  return (
    <div>
      <AddTask />
    </div>
  );
};

export default App;

If you try to submit the form now you will see the data in your firestore.

Now we will work on retrieving data from firestore. In App.js at the top paste the following

import { collection, query, onSnapshot } from "firebase/firestore";
import { useEffect, useState } from "react";

after that we will create initial state which will be an empty array and will populate data from useEffect

const [tasks, setTasks] = useState([]);

useEffect(() => {
  const q = query(collection(db, "tasks"));
  const unsub = onSnapshot(q, (querySnapshot) => {
    let tasksArray = [];
    querySnapshot.forEach((doc) => {
      tasksArray.push({ ...doc.data(), id: doc.id });
    });
    setTasks(tasksArray);
  });
  return () => unsub();
}, []);

In useEffect first we created a query object, query functions takes collection as argument and collection takes firestore reference and collection name as arguments.

After that we executed query, we are using onSnapshot function (realtime listener) which takes query and returns querySnapshot, then we looped over querySnapshot and pushed data to the array and finally we passed the array to state. Now in return statement below <AddTask /> paste the following code

<div className="task_container">
  {tasks.map((task) => (
    <Task
      key={task.id}
      task={task}
    />
  ))}
</div>

Here we run a map method on array and passed the task to Task component. Create a file Task.js in components directory and paste the following code.

import React from "react";

const Task = ({ task }) => {
  return (
    <div className="task">
      <p
        style={{ textDecoration: task.completed && "line-through" }}
      >
        {task.title}
      </p>

      <button>X</button>
    </div>
  );
};

export default Task;

Now goto App.js and import doc, updateDoc, and deleteDoc from firebase/firestore. and paste the following code after useEffect

const toggleComplete = async (task) => {
    await updateDoc(doc(db, "tasks", task.id), { completed: !task.completed });
  };

  const handleDelete = async (id) => {
    await deleteDoc(doc(db, "tasks", id));
  };

replace the Task component in App.js with the following

<Task
    key={task.id}
    task={task}
    toggleComplete={toggleComplete}
    handleDelete={handleDelete}
   />

toggleComplete function will toggle the task if it is completed upon click it will set completed to false and vice versa, We used updateDoc function which takes doc as argument and doc takes firestore reference, collection name, document identifier and fields we want to update.

handleDelete function will delete the task from firestore. We used deleteDoc from firebase/firestore, passed doc to it and doc takes firestore reference, collection name and document identifier as arguments.

Now in components/Task.js receive toggleComplete and handleDelete as arguments. Replace the Task.js with the following

import React from "react";

const Task = ({ task, toggleComplete, handleDelete }) => {
  return (
    <div className="task">
      <p
        style={{ textDecoration: task.completed && "line-through" }}
        onClick={() => toggleComplete(task)}
      >
        {task.title}
      </p>

      <button onClick={() => handleDelete(task.id)}>X</button>
    </div>
  );
};

export default Task;

We are calling toggleComplete function when we click on <p> tag and passed task to it. On button click we are calling handleDelete function which takes task id.

In src/App.css replace the existing styles with the following

* {
  box-sizing: border-box;
}
body {
  background-color: #eee;
}
form {
  max-width: 500px;
  margin: 0 auto;
  background: white;
  padding: 10px 20px;
  margin-top: 50px;
  border-radius: 5px;
  box-shadow: 1px 2px 10px #ddd;
}
form h3 {
  text-align: center;
}
form input {
  width: 100%;
  padding: 10px 5px;
  outline: none;
  font-size: 16px;
}
.input_container {
  margin-bottom: 20px;
}
.btn_container {
  text-align: center;
  margin-bottom: 10px;
}
.btn_container button {
  background: #333;
  color: white;
  padding: 10px 20px;
  outline: none;
  border: none;
  border-radius: 5px;
  font-size: 16px;
  cursor: pointer;
}

.task_container {
  max-width: 1200px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 5px;
  margin-top: 20px;
}
.task {
  margin-top: 20px;
  margin-right: 10px;
  background: white;
  padding: 10px 20px;
  border-radius: 5px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  box-shadow: 1px 2px 10px #ddd;
}
.task p {
  cursor: pointer;
}
.task button {
  background: #eb4034;
  color: white;
  outline: none;
  border: none;
  border-radius: 5px;
  padding: 5px 10px;
  margin-left: 10px;
  cursor: pointer;
}
@media screen and (max-width: 667px) {
  .task_container {
    grid-template-columns: 1fr;
  }
  .task {
    margin-right: 0px;
  }
}

We now have a fully functional CRUD app. Give it a try.

Github repo

Share

Privacy Policy
icon
icon
icon
icon

Developed By Farhan Farooq