React Hooks Firestore example: Build a CRUD app

In this tutorial, I will show you step by step to build a React Hooks Firestore CRUD App example.

Related Posts:
React Hooks CRUD with Firebase Realtime Database
React Hooks CRUD example with Axios and Web API
React Hooks: JWT Authentication (without Redux) example
React Redux Login, Logout, Registration example with Hooks
React Form Validation with Hooks example


React Firestore CRUD Overview

We’re gonna build an React Firestore App using firebase library in which:

  • Each Tutorial has id, title, description, published status.
  • We can create, retrieve, update, delete Tutorials (CRUD operations) from Firebase Cloud Firestore

Here are the screenshots:

– Create a new Tutorial:

react-hooks-firestore-example-crud-app-create

Cloud Firestore after the Create Operations:

react-hooks-firestore-example-crud-app-cloud-firestore-data-view

– Retrieve all Tutorials, the details will show when clicking on any Tutorial:

react-hooks-firestore-example-crud-app-retrieve

– Change status to Published/Pending using Publish/UnPublish button:

react-hooks-firestore-example-crud-app-update-status

– Update the Tutorial details with Update button:

react-hooks-firestore-example-crud-app-update

– Delete the Tutorial using Delete button:

react-hooks-firestore-example-crud-app-delete

For Form Validation, please visit:
React Form Validation with Hooks example

CRUD Operations using Firestore CollectionReference

We’re gonna use instance of firebase.firestore.CollectionReference to read/write data from the Firestore.

var tutorialsRef = firebase.firestore().collection("/tutorials");

– Read collection once using get():

tutorialsRef.get().then(function(snapshot) {
  vat tutorials = [];

  snapshot.forEach(function(childSnapshot) {
    var id = childSnapshot.id;
    var data = childSnapshot.val();
    // ...

    tutorials.push({ id: id, title: data.title, description: data.description});
  });
});

– Read collection with listening to the data changes using onSnapshot():

tutorialsRef.onSnapshot(function(snapshot) {
  snapshot.docChanges().forEach(function(change) {
    if (change.type === "added") {
      console.log("New tutorial: ", change.doc.data());
    }
    if (change.type === "modified") {
      console.log("Modified tutorial: ", change.doc.data());
    }
    if (change.type === "removed") {
      console.log("Removed tutorial: ", change.doc.data());
    }
  });
});

– Listening for all value change events on a collection reference

tutorialsRef.onSnapshot(function(snapshot) {
  snapshot.forEach(function(childSnapshot) {
    var id = childSnapshot.id;
    var childData = childSnapshot.val();
    // ...
  });
});

– Detach the listener to stop using bandwidth to receive updates:

var unsubscribe = tutorialsRef.onSnapshot(function(snapshot) {
  // ...
});

// Stop listening to changes
unsubscribe();

– Create a new document in collection using add():

tutorialsRef.add({
  title: "bezkoder Tut#1",
  description: "Helpful tutorial"
})
.then(function(docRef) {
    console.log("Tutorial created with ID: ", docRef.id);
})
.catch(function(error) {
    console.error("Error adding Tutorial: ", error);
});;

– Update document by id in collection:
+ destructive update using set(): delete everything currently in place, then save the new value

tutorialsRef.doc(id).set({
  title: 'zkoder Tut#1',
  description: 'Tut#1 Description'
});

+ non-destructive update using update(): only updates the specified values

tutorialsRef.doc(id).update({
  title: 'zkoder new Tut#1'
});

– Delete a document by id in collection:

tutorialsRef.doc(id).delete();

– Delete entire collection: Deleting Firestore collections from a Web client is not recommended.
You can find the solution here.

Technology

  • React 16
  • firebase 8/7
  • bootstrap 4

Setup the Firebase Project

Go to Firebase Console, login with your Google Account, then click on Add Project.

You will see the window like this:

react-hooks-firestore-example-crud-create-firebase-project

Enter Project name, set Project Id and click on Continue.
Turn off Enable Google Analytics for this project, then click Create Project.

Now, browser turns into following view:

react-hooks-firestore-example-crud-choose-web-app

If you don’t see it, just choose Project Overview.
Click on Web App, a window will be shown:

react-hooks-firestore-example-crud-example-register-app

Set the nickname and choose Register App for next step.

react-hooks-firestore-example-crud-app-add-firebase-sdk

Copy the script for later use.

Choose Cloud Firestore on the left (list of Firebase features) -> Create Database.

In this tutorial, we don’t implement Authentication, so let’s choose test mode:

react-hooks-firestore-example-crud-app-set-rules

Or if you come from another situation, just open Tab Rules, then change allow read, write value to true.

Finally, we need to set Cloud Firestore Location:

react-hooks-firestore-example-crud-app-set-cloud-firestore-location

Setup React Hooks Project

Open cmd at the folder you want to save Project folder, run command:
npx create-react-app react-firestore-crud

After the process is done. We create additional folders and files like the following tree:


public

src

components

AddTutorial.js

Tutorial.js

TutorialsList.js

services

TutorialService.js

App.css

App.js

firebase.js

index.js

package.json


Let me explain it briefly.

firebase.js configures information to connect with Firebase Project and export Firebase Firestore service.
services/tutorial.service.js exports TutorialDataService that uses firebase‘s Firestore CollectionReference to interact with Firestore.
– There are 3 pages that uses TutorialDataService:

  • AddTutorial for creating new item
  • TutorialsList contains list of items, parent of Tutorial
  • Tutorial shows item details

App.js contains Browser Router view and navigation bar.

Import Bootstrap to React Hooks Firestore CRUD App

Run command: npm install bootstrap.

Open src/App.js and modify the code inside it as following-

import React from "react";
import "bootstrap/dist/css/bootstrap.min.css";

function App() {
  return (
    // ...
  );
}

export default App;

Add React Router to React Hooks Firestore App

– Run the command: npm install react-router-dom.
– Open src/index.js and wrap App component by BrowserRouter object.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { BrowserRouter } from "react-router-dom";
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root')
);

reportWebVitals();

Add Navbar to React Hooks Firestore App

Open src/App.js, this App component is the root container for our application, it will contain a navbar, and also, a Switch object with several Route. Each Route points to a React Page.

There are 2 main routes:

  • /add for AddTutorial page
  • /tutorials for TutorialsList page
import React from "react";
import { Switch, Route, Link } from "react-router-dom";
import "bootstrap/dist/css/bootstrap.min.css";
import "./App.css";

import AddTutorial from "./components/AddTutorial";
import TutorialsList from "./components/TutorialsList";

function App() {
  return (
    <div>
      <nav className="navbar navbar-expand navbar-dark bg-dark">
        <a href="/tutorials" className="navbar-brand">
          bezKoder
        </a>
        <div className="navbar-nav mr-auto">
          <li className="nav-item">
            <Link to={"/tutorials"} className="nav-link">
              Tutorials
            </Link>
          </li>
          <li className="nav-item">
            <Link to={"/add"} className="nav-link">
              Add
            </Link>
          </li>
        </div>
      </nav>

      <div className="container mt-3">
        <h2>React Hooks Firestore example</h2>
        <Switch>
          <Route exact path={["/", "/tutorials"]} component={TutorialsList} />
          <Route exact path="/add" component={AddTutorial} />
        </Switch>
      </div>
    </div>
  );
}

export default App;

Integrate Firebase into React App

First run the command: npm install firebase.

Open src/firebase.js, import firebase library and add configuration that we have saved when Popup window was shown:

import firebase from "firebase/app";
import "firebase/firestore";

let config = {
  apiKey: "xxx",
  authDomain: "bezkoder-firebase.firebaseapp.com",
  databaseURL: "https://bezkoder-firebase.firebaseio.com",
  projectId: "bezkoder-firebase",
  storageBucket: "bezkoder-firebase.appspot.com",
  messagingSenderId: "xxx",
  appId: "xxx",
};

firebase.initializeApp(config);

export default firebase.firestore();

Don’t forget to export firebase.firestore.Firestore service with firebase.firestore().

Create Data Service

This service will use Firestore service exported above to interact with Firebase Cloud Firestore. It contains necessary functions for CRUD operations.

services/tutorial.service.js

import firebase from "../firebase";

const db = firebase.collection("/tutorials");

const getAll = () => {
  return db;
};

const create = (data) => {
  return db.add(data);
};

const update = (id, value) => {
  return db.doc(id).update(value);
};

const remove = (id) => {
  return db.doc(id).delete();
};

const TutorialService = {
  getAll,
  create,
  update,
  remove
};

export default TutorialService;

Page for creating Document

This page has a Form to submit new Tutorial with 3 fields: title, description & published (false by default). It calls TutorialDataService.create() method.

components/AddTutorial.js

import React, { useState } from "react";
import TutorialDataService from "../services/TutorialService";

const AddTutorial = () => {
  const initialTutorialState = {
    title: "",
    description: "",
    published: false
  };
  const [tutorial, setTutorial] = useState(initialTutorialState);
  const [submitted, setSubmitted] = useState(false);

  const handleInputChange = event => {
    const { name, value } = event.target;
    setTutorial({ ...tutorial, [name]: value });
  };

  const saveTutorial = () => {
    var data = {
      title: tutorial.title,
      description: tutorial.description,
      published: false
    };

    TutorialDataService.create(data)
      .then(() => {
        setSubmitted(true);
      })
      .catch(e => {
        console.log(e);
      });
  };

  const newTutorial = () => {
    setTutorial(initialTutorialState);
    setSubmitted(false);
  };

  return (
    ...
  );
};

export default AddTutorial;

First, we define the constructor and set initial state, bind this to the different events.

Because there are 2 fields, so we create 2 functions to track the values of the input and set that state for changes. We also have a function to get value of the form (state) and call TutorialDataService.create() method.

For render() method, we check the submitted state, if it is true, we show Add button for creating new Tutorial again. Otherwise, a Form will display.

const AddTutorial = () => {
  ...

  return (
    <div className="submit-form">
      {submitted ? (
        <div>
          <h4>You submitted successfully!</h4>
          <button className="btn btn-success" onClick={newTutorial}>
            Add
          </button>
        </div>
      ) : (
        <div>
          <div className="form-group">
            <label htmlFor="title">Title</label>
            <input
              type="text"
              className="form-control"
              id="title"
              required
              value={tutorial.title}
              onChange={handleInputChange}
              name="title"
            />
          </div>

          <div className="form-group">
            <label htmlFor="description">Description</label>
            <input
              type="text"
              className="form-control"
              id="description"
              required
              value={tutorial.description}
              onChange={handleInputChange}
              name="description"
            />
          </div>

          <button onClick={saveTutorial} className="btn btn-success">
            Submit
          </button>
        </div>
      )}
    </div>
  );
};

export default AddTutorial;

Page for List of Documents

This page has:

  • a tutorials array displayed as a list on the left.
  • a selected Tutorial which is shown on the right.

react-hooks-firestore-example-crud-app-retrieve

So we will have following state:

  • tutorials
  • currentTutorial and currentIndex

We also need to use TutorialDataService‘s getAll() method with .orderBy("title", "asc") for sort the list by title field in ascending order.

components/TutorialsList.js

import React, { useState, useEffect } from "react";
import TutorialDataService from "../services/TutorialService";
import Tutorial from "./Tutorial";

const TutorialsList = () => {
  const [tutorials, setTutorials] = useState([]);
  const [currentTutorial, setCurrentTutorial] = useState(null);
  const [currentIndex, setCurrentIndex] = useState(-1);

  const onDataChange = (items) => {
    let tutorials = [];

    items.docs.forEach((item) => {
      let id = item.id;
      let data = item.data();
      tutorials.push({
        id: id,
        title: data.title,
        description: data.description,
        published: data.published,
      });
    });

    setTutorials(tutorials);
  };

  useEffect(() => {
    const unsubscribe = TutorialDataService.getAll().orderBy("title", "asc").onSnapshot(onDataChange);

    return () => unsubscribe();
  }, []);


  const refreshList = () => {
    setCurrentTutorial(null);
    setCurrentIndex(-1);
  };

  const setActiveTutorial = (tutorial, index) => {
    const { title, description, published } = tutorial;

    setCurrentTutorial({
      id: tutorial.id,
      title,
      description,
      published,
    });

    setCurrentIndex(index);
  };
  
  return (
    ...
  );
};

export default TutorialsList;

In the code above, we add a listener for data value changes and detach the listener using useEffect() hook.

Inside listener function, we get the id and other fields of each item. This id is unique and important for update operation.

We also have refreshList() function for every time delete operation is done (in child component).

Let’s continue to return the template:

const TutorialsList = () => {
  ...

  return (
    <div className="list row">
      <div className="col-md-6">
        <h4>Tutorials List</h4>

        <ul className="list-group">
          { tutorials &&
            tutorials.map((tutorial, index) =>
              <li
                className={"list-group-item " + (index === currentIndex ? "active" : "")}
                onClick={() => setActiveTutorial(tutorial, index)}
                key={tutorial.id}
              >
                { tutorial.title }
              </li>
            ))}
        </ul>
      </div>
      <div className="col-md-6">
        {currentTutorial ? (
          <Tutorial tutorial={currentTutorial} refreshList={refreshList} />
        ) : (
          <div>
            <br />
            <p>Please click on a Tutorial...</p>
          </div>
        )}
      </div>
    </div>
  );
};

export default TutorialsList;

You can see that when we click on any item, setActiveTutorial() function will be invoked to change current active Tutorial, which data is passed to tutorial component.

Using react-firebase-hooks instead

Let’s modify the TutorialsList page to take advantage of react-firebase-hooks useCollection() hook.

You have error and loading property to give a complete lifecycle for loading and listening to the Database Reference, but don’t need to add/detach listener within useEffect() hook.

import React, { useState } from "react";
import { useCollection } from "react-firebase-hooks/firestore";
import TutorialDataService from "../services/TutorialService";
import Tutorial from "./Tutorial";

const TutorialsList = () => {
  const [currentTutorial, setCurrentTutorial] = useState(null);
  const [currentIndex, setCurrentIndex] = useState(-1);

  const [tutorials, loading, error] = useCollection(TutorialDataService.getAll().orderBy("title", "asc"));

  const refreshList = () => {
    setCurrentTutorial(null);
    setCurrentIndex(-1);
  };

  const setActiveTutorial = (tutorial, index) => {
    const { title, description, published } = tutorial.data();

    setCurrentTutorial({
      id: tutorial.id,
      title,
      description,
      published,
    });

    setCurrentIndex(index);
  };

  return (
    <div className="list row">
      <div className="col-md-6">
        <h4>Tutorials List</h4>
        {error && <strong>Error: {error}</strong>}
        {loading && <span>Loading...</span>}
        <ul className="list-group">
          { !loading &&
            tutorials &&
            tutorials.docs.map((tutorial, index) => (
              <li
                className={"list-group-item " + (index === currentIndex ? "active" : "")}
                onClick={() => setActiveTutorial(tutorial, index)}
                key={tutorial.id}
              >
                { tutorial.data().title }
              </li>
            ))}
        </ul>
      </div>
      <div className="col-md-6">
        {currentTutorial ? (
          <Tutorial tutorial={currentTutorial} refreshList={refreshList} />
        ) : (
          <div>
            <br />
            <p>Please click on a Tutorial...</p>
          </div>
        )}
      </div>
    </div>
  );
};

export default TutorialsList;

Page for Document details

This page is the child of TutorialsList. It bind tutorial data and invoke refreshList of the parent.

For getting update, delete the Tutorial, we’re gonna use two TutorialDataService methods:

  • update()
  • remove()

components/Tutorial.js

import React, { useState } from "react";
import TutorialDataService from "../services/TutorialService";

const Tutorial = (props) => {
  const initialTutorialState = {
    key: null,
    title: "",
    description: "",
    published: false,
  };
  const [currentTutorial, setCurrentTutorial] = useState(initialTutorialState);
  const [message, setMessage] = useState("");

  const { tutorial } = props;
  if (currentTutorial.id !== tutorial.id) {
    setCurrentTutorial(tutorial);
    setMessage("");
  }

  const handleInputChange = (event) => {
    const { name, value } = event.target;
    setCurrentTutorial({ ...currentTutorial, [name]: value });
  };

  const updatePublished = (status) => {
    TutorialDataService.update(currentTutorial.id, { published: status })
      .then(() => {
        setCurrentTutorial({ ...currentTutorial, published: status });
        setMessage("The status was updated successfully!");
      })
      .catch((e) => {
        console.log(e);
      });
  };

  const updateTutorial = () => {
    const data = {
      title: currentTutorial.title,
      description: currentTutorial.description,
    };

    TutorialDataService.update(currentTutorial.id, data)
      .then(() => {
        setMessage("The tutorial was updated successfully!");
      })
      .catch((e) => {
        console.log(e);
      });
  };

  const deleteTutorial = () => {
    TutorialDataService.remove(currentTutorial.id)
      .then(() => {
        props.refreshList();
      })
      .catch((e) => {
        console.log(e);
      });
  };

  return (
    ...
  );
};

export default Tutorial;

And this is the code inside return:

const Tutorial = (props) => {
  ...

  return (
    <div>
      {currentTutorial ? (
        <div className="edit-form">
          <h4>Tutorial</h4>
          <form>
            <div className="form-group">
              <label htmlFor="title">Title</label>
              <input
                type="text"
                className="form-control"
                id="title"
                name="title"
                value={currentTutorial.title}
                onChange={handleInputChange}
              />
            </div>
            <div className="form-group">
              <label htmlFor="description">Description</label>
              <input
                type="text"
                className="form-control"
                id="description"
                name="description"
                value={currentTutorial.description}
                onChange={handleInputChange}
              />
            </div>

            <div className="form-group">
              <label>
                <strong>Status:</strong>
              </label>
              {currentTutorial.published ? "Published" : "Pending"}
            </div>
          </form>

          {currentTutorial.published ? (
            <button
              className="badge badge-primary mr-2"
              onClick={() => updatePublished(false)}
            >
              UnPublish
            </button>
          ) : (
            <button
              className="badge badge-primary mr-2"
              onClick={() => updatePublished(true)}
            >
              Publish
            </button>
          )}

          <button className="badge badge-danger mr-2" onClick={deleteTutorial}>
            Delete
          </button>

          <button
            type="submit"
            className="badge badge-success"
            onClick={updateTutorial}
          >
            Update
          </button>
          <p>{message}</p>
        </div>
      ) : (
        <div>
          <br />
          <p>Please click on a Tutorial...</p>
        </div>
      )}
    </div>
  );
};

export default Tutorial;

Add CSS style for React Pages

Open src/App.css and write some CSS code as following:

.container h2 {
  text-align: center;
  margin: 25px auto;
}

.list {
  text-align: left;
  max-width: 750px;
  margin: auto;
}

.submit-form {
  max-width: 300px;
  margin: auto;
}

.edit-form {
  max-width: 300px;
  margin: auto;
}

Run & Check

You can run this App with command: npm start.

Compiled successfully!

You can now view react-hooks-firestore in the browser.

  Local:            http://localhost:3000      
  On Your Network:  http://192.168.1.6:3000

Conclusion

Today we’ve built React Hooks Firestore CRUD Application successfully working with Cloud Firestore using firebase library. Now we can display, modify, delete documents and collection at ease.

You can also find how to create React HTTP Client for working with Restful API in:
React Hooks CRUD example with Axios and Web API

Form Validation:
React Form Validation with Hooks example

Or Firebase Realtime Database for serverless:
React Hooks CRUD with Firebase Realtime Database

Happy learning, see you again!

Further Reading

Security:

Fullstack:

Source Code

You can find the complete source code for this tutorial on Github.

Leave a Reply

Your email address will not be published. Required fields are marked *