Creating a CRUD App with React, TypeScript, Vite, and Firebase
- With Code Example
- July 15, 2024
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!
Prerequisites
Ensure you have the following installed:
- Node.js and npm (Node Package Manager)
- A code editor (e.g., Visual Studio Code)
- 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
- Go to the Firebase Console (https://console.firebase.google.com/) and create a new project.
- Once your project is created, click on “Add app” and choose the web option.
- 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:
CreateItem.tsx
ReadItems.tsx
UpdateItem.tsx
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;
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;
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;
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.