Creating a CRUD App with React, TypeScript, Vite, and Firebase

Build a scalable CRUD app with React, TypeScript, and Firebase

In this guide, we’ll walk through creating a CRUD (Create, Read, Update, Delete) application using React with TypeScript, Vite for our build tool, and Firebase for the backend. Let’s get started!

react-firebase_BGrgpFAt_10867108491625030287.jpg

Prerequisites

Ensure you have the following installed:

  1. Node.js and npm (Node Package Manager)
  2. A code editor (e.g., Visual Studio Code)
  3. Basic knowledge of React, TypeScript, and JavaScript

Step 1: Setting Up the React Project with Vite

First, let’s create a new React project using Vite with TypeScript:

npm create vite@latest react-firebase-crud -- --template react-ts
cd react-firebase-crud
npm install

Install Tailwind CSS

Follow the quick guide to install and start using the tailwind css in your react application. Use Tailwind CSS with React

This creates a new React project with TypeScript and Vite, then navigates into its directory and installs the necessary dependencies.

Step 2: Installing Additional Dependencies

We need to install Firebase and React Router:

npm install firebase react-router-dom

Step 3: Setting Up Firebase

firebase

  1. Go to the Firebase Console (https://console.firebase.google.com/) and create a new project.
  2. Once your project is created, click on “Add app” and choose the web option.
  3. Follow the instructions to register your app and copy the configuration object.

Now, create a new file called firebase.ts in your src folder and add the following code:

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

const firebaseConfig = {
  // Your Firebase configuration object goes here
};

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

Replace the firebaseConfig object with the configuration you copied from the Firebase Console.

Step 4: Creating the CRUD Components

Let’s create four components for our CRUD operations. In the src folder, create a new folder called components and add the following files:

  1. CreateItem.tsx
  2. ReadItems.tsx
  3. UpdateItem.tsx
  4. DeleteItem.tsx

CreateItem.tsx

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

const CreateItem: React.FC = () => {
  const [name, setName] = useState('');
  const [description, setDescription] = useState('');

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      await addDoc(collection(db, 'items'), {
        name,
        description,
      });
      setName('');
      setDescription('');
      alert('Item added successfully!');
    } catch (error) {
      console.error('Error adding item: ', error);
    }
  };

  return (
    <div className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
      <h2 className="text-2xl font-bold mb-4">Create New Item</h2>
      <form onSubmit={handleSubmit}>
        <div className="mb-4">
          <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
            Item Name
          </label>
          <input
            className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
            id="name"
            type="text"
            value={name}
            onChange={(e) => setName(e.target.value)}
            placeholder="Item name"
            required
          />
        </div>
        <div className="mb-6">
          <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="description">
            Description
          </label>
          <textarea
            className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
            id="description"
            value={description}
            onChange={(e) => setDescription(e.target.value)}
            placeholder="Item description"
            required
          />
        </div>
        <div className="flex items-center justify-between">
          <button
            className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
            type="submit"
          >
            Add Item
          </button>
        </div>
      </form>
    </div>
  );
}

export default CreateItem;

create items

ReadItems.tsx

import React, { useState, useEffect } from 'react';
import { collection, getDocs } from 'firebase/firestore';
import { db } from '../firebase';
import { Link } from 'react-router-dom';

interface Item {
  id: string;
  name: string;
  description: string;
}

const ReadItems: React.FC = () => {
  const [items, setItems] = useState<Item[]>([]);

  useEffect(() => {
    const fetchItems = async () => {
      const querySnapshot = await getDocs(collection(db, 'items'));
      const itemsList = querySnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() } as Item));
      setItems(itemsList);
    };
    fetchItems();
  }, []);

  return (
    <div>
      <h2 className="text-2xl font-bold mb-4">Items List</h2>
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
        {items.map(item => (
          <div key={item.id} className="bg-white shadow-md rounded-lg p-6">
            <h3 className="text-xl font-semibold mb-2">{item.name}</h3>
            <p className="text-gray-600 mb-4">{item.description}</p>
            <div className="flex justify-between">
              <Link to={`/update/${item.id}`} className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded">
                Edit
              </Link>
              <Link to={`/delete/${item.id}`} className="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded">
                Delete
              </Link>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

export default ReadItems;

Read items

UpdateItem.tsx

import React, { useState } from 'react';
import { doc, updateDoc } from 'firebase/firestore';
import { db } from '../firebase';

interface UpdateItemProps {
  id: string;
  name: string;
  description: string;
}

const UpdateItem: React.FC<UpdateItemProps> = ({ id, name: initialName, description: initialDescription }) => {
  const [name, setName] = useState(initialName);
  const [description, setDescription] = useState(initialDescription);

  const handleUpdate = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      const itemRef = doc(db, 'items', id);
      await updateDoc(itemRef, { name, description });
      alert('Item updated successfully!');
    } catch (error) {
      console.error('Error updating item: ', error);
    }
  };

  return (
    <div className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
      <h2 className="text-2xl font-bold mb-4">Update Item</h2>
      <form onSubmit={handleUpdate}>
        <div className="mb-4">
          <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
            Item Name
          </label>
          <input
            className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
            id="name"
            type="text"
            value={name}
            onChange={(e) => setName(e.target.value)}
            placeholder="Item name"
            required
          />
        </div>
        <div className="mb-6">
          <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="description">
            Description
          </label>
          <textarea
            className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
            id="description"
            value={description}
            onChange={(e) => setDescription(e.target.value)}
            placeholder="Item description"
            required
          />
        </div>
        <div className="flex items-center justify-between">
          <button
            className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
            type="submit"
          >
            Update Item
          </button>
        </div>
      </form>
    </div>
  );
}

export default UpdateItem;

DeleteItem.tsx

import React from 'react';
import { doc, deleteDoc } from 'firebase/firestore';
import { db } from '../firebase';
import { useNavigate } from 'react-router-dom';

interface DeleteItemProps {
  id: string;
}

const DeleteItem: React.FC<DeleteItemProps> = ({ id }) => {
  const navigate = useNavigate();

  const handleDelete = async () => {
    try {
      await deleteDoc(doc(db, 'items', id));
      alert('Item deleted successfully!');
      navigate('/');
    } catch (error) {
      console.error('Error deleting item: ', error);
    }
  };

  return (
    <div className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
      <h2 className="text-2xl font-bold mb-4">Delete Item</h2>
      <p className="mb-4">Are you sure you want to delete this item?</p>
      <button
        onClick={handleDelete}
        className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
      >
        Delete Item
      </button>
    </div>
  );
}

export default DeleteItem;

delete item

Step 5: Putting It All Together

Now let’s update the App.tsx file to bring everything together:

import React from 'react';
import { BrowserRouter as Router, Route, Routes, Link } from 'react-router-dom';
import CreateItem from './components/CreateItem';
import ReadItems from './components/ReadItems';
import UpdateItem from './components/UpdateItem';
import DeleteItem from './components/DeleteItem';

const App: React.FC = () => {
  return (
    <Router>
      <div className="min-h-screen bg-gray-100">
        <nav className="bg-white shadow-lg">
          <div className="max-w-6xl mx-auto px-4">
            <div className="flex justify-between">
              <div className="flex space-x-7">
                <div>
                  <Link to="/" className="flex items-center py-4 px-2">
                    <span className="font-semibold text-gray-500 text-lg">CRUD App</span>
                  </Link>
                </div>
              </div>
              <div className="flex items-center space-x-3">
                <Link to="/" className="py-4 px-2 text-gray-500 hover:text-gray-900">Home</Link>
                <Link to="/create" className="py-2 px-2 font-medium text-white bg-green-500 rounded hover:bg-green-400 transition duration-300">Create Item</Link>
              </div>
            </div>
          </div>
        </nav>

        <div className="max-w-6xl mx-auto mt-8 px-4">
          <Routes>
            <Route path="/" element={<ReadItems />} />
            <Route path="/create" element={<CreateItem />} />
            <Route path="/update/:id" element={<UpdateItem id="" name="" description="" />} />
            <Route path="/delete/:id" element={<DeleteItem id="" />} />
          </Routes>
        </div>
      </div>
    </Router>
  );
}

export default App;

Note: The UpdateItem and DeleteItem components in the routes are given placeholder props. In a real application, you’d pass the actual item data to these components based on the route parameters.

Step 6: Running the Application

Now that we have everything set up, let’s run our application:

npm run dev

Your CRUD app should now be running, typically on http://localhost:5173 (Vite’s default port).

Conclusion

Congratulations! You’ve successfully created a CRUD application using React with TypeScript, Vite, and Firebase. This app allows you to create, read, update, and delete items in a Firebase database.

Remember to handle errors more gracefully in a production environment, add appropriate styling to make your app look great, and consider adding more robust type checking throughout your application.