React and Firebase make a powerful combo for building real-time web apps. In this tutorial, weโll walk through building a Firestore CRUD app using the latest versions:
-
โ๏ธ React 19.1.0 with Hooks & functional components
-
๐ฅ Firebase v10+ using modular imports
-
๐ฃ๏ธ React Router v6
-
โ No class components, no deprecated APIs
The result will be a clean and modern CRUD web application.
Prerequisites
-
Node.js v18+ and npm
-
Firebase project (go to console.firebase.google.com)
-
Firestore is enabled in test mode
-
Basic React knowledge
1. Set Up Your React Project
Create the app using Vite (or CRA):
npm create vite@latest react-firebase-crud --template react
cd react-firebase-crud
npm install
Then install dependencies:
npm install firebase react-router-dom@6
2. Firebase Setup
2.1 Create Firebase Project
-
Go to console.firebase.google.com
-
Create a project, then:
-
Add a Web app
-
Copy the Firebase config
-
Enable Firestore in test mode
-
2.2 Create firebase.ts
// src/firebase.ts
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_PROJECT.firebaseapp.com",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_PROJECT.appspot.com",
messagingSenderId: "SENDER_ID",
appId: "APP_ID",
};
const app = initializeApp(firebaseConfig);
export const db = getFirestore(app);
3. App Structure
src/
โโโ components/
โ โโโ BoardList.tsx
โ โโโ BoardAdd.tsx
โ โโโ BoardEdit.tsx
โ โโโ BoardShow.tsx
โโโ types/
โ โโโ Board.ts
โโโ firebase.ts
โโโ App.tsx
โโโ main.tsx
Add those directories and files.
mkdir src/components
touch src/components/BoardList.tsx
touch src/components/BoardAdd.tsx
touch src/components/BoardEdit.tsx
touch src/components/BoardShow.tsx
mkdir src/types
touch src/types/Board.ts
4. Routing Setup
main.tsx
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";
import { BrowserRouter } from "react-router-dom";
createRoot(document.getElementById("root")!).render(
<BrowserRouter>
<App />
</BrowserRouter>
);
App.tsx
import { Routes, Route, Link } from 'react-router-dom';
import BoardList from './components/BoardList';
import BoardAdd from './components/BoardAdd';
import BoardEdit from './components/BoardEdit';
import BoardShow from './components/BoardShow';
function App() {
return (
<div className="container">
<h1>React Firebase Firestore CRUD</h1>
<nav>
<Link to="/">Boards</Link> | <Link to="/add">Add</Link>
</nav>
<Routes>
<Route path="/" element={<BoardList />} />
<Route path="/add" element={<BoardAdd />} />
<Route path="/edit/:id" element={<BoardEdit />} />
<Route path="/show/:id" element={<BoardShow />} />
</Routes>
</div>
);
}
export default App;
5. CRUD Components
5.1 src/types/Board.ts
// src/types/Board.ts
export interface Board {
id?: string;
title: string;
description: string;
}
5.2 src/components/BoardList.tsx
import { useEffect, useState } from "react";
import { db } from "../firebase";
import { collection, getDocs, deleteDoc, doc } from "firebase/firestore";
import { Link } from "react-router-dom";
import type { Board } from "../types/Board";
function BoardList() {
const [boards, setBoards] = useState<Board[]>([]);
useEffect(() => {
const fetchBoards = async () => {
const snapshot = await getDocs(collection(db, "boards"));
setBoards(
snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() } as Board))
);
};
fetchBoards();
}, []);
const deleteBoard = async (id: string) => {
await deleteDoc(doc(db, "boards", id));
setBoards(boards.filter((board) => board.id !== id));
};
return (
<div>
<h2>Board List</h2>
<ul>
{boards.map((board) => (
<li key={board.id}>
<Link to={`/show/${board.id}`}>{board.title}</Link>{" "}
<Link to={`/edit/${board.id}`}>Edit</Link>{" "}
<button onClick={() => deleteBoard(board.id!)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
export default BoardList;
5.2 BoardAdd.jsx
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { db } from "../firebase";
import { collection, addDoc } from "firebase/firestore";
function BoardAdd() {
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const navigate = useNavigate();
const onSubmit = async (e: any) => {
e.preventDefault();
await addDoc(collection(db, "boards"), { title, description });
navigate("/");
};
return (
<form onSubmit={onSubmit}>
<h2>Add Board</h2>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Title"
required
/>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Description"
required
/>
<button type="submit">Save</button>
</form>
);
}
export default BoardAdd;
5.3 BoardEdit.jsx
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { db } from "../firebase";
import { doc, getDoc, updateDoc } from "firebase/firestore";
import type { Board } from "../types/Board";
function BoardEdit() {
const [board, setBoard] = useState<Board | undefined>();
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
useEffect(() => {
if (!id) return;
const loadBoard = async () => {
const snap = await getDoc(doc(db, "boards", id));
setBoard(snap.data() as Board);
};
loadBoard();
}, [id]);
const onSubmit = async (e: any) => {
if (!id || !board) return;
e.preventDefault();
await updateDoc(doc(db, "boards", id), { ...board });
navigate("/");
};
return (
<form onSubmit={onSubmit}>
<h2>Edit Board</h2>
<input
value={board!.title}
onChange={(e) => {
if (!board) return;
setBoard({ ...board, title: e.target.value });
}}
required
/>
<textarea
value={board!.description}
onChange={(e) => {
if (!board) return;
setBoard({ ...board, description: e.target.value });
}}
required
/>
<button type="submit">Update</button>
</form>
);
}
export default BoardEdit;
5.4 BoardShow.jsx
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { db } from "../firebase";
import { doc, getDoc } from "firebase/firestore";
import type { Board } from "../types/Board";
function BoardShow() {
const [board, setBoard] = useState<Board | undefined>();
const { id } = useParams();
useEffect(() => {
if (!id) return;
const fetchBoard = async () => {
const snap = await getDoc(doc(db, "boards", id));
setBoard(snap.data() as Board);
};
fetchBoard();
}, [id]);
return board ? (
<div>
<h2>{board.title}</h2>
<p>{board.description}</p>
</div>
) : (
<p>Loading...</p>
);
}
export default BoardShow;
Test It Out
npm run dev
-
Navigate to
/add
, create a board -
See it listed on
/
-
Click to view, edit, or delete
Conclusion
Youโve now built a full-featured CRUD Firestore App using:
-
โ React 19.1.0
-
โ Firebase 10+ modular SDK
-
โ React Router 6
-
โ Functional components and hooks
This codebase is cleaner, modern, and future-ready.
You can find the full working source code on our GitHub.
That's just the basics. If you need more deep learning about React, you can take the following cheap course:
- React - The Complete Guide 2025 (incl. Next.js, Redux)
- The Ultimate React Course 2025: React, Next.js, Redux & More
- Modern React From The Beginning
- Complete React, Next.js & TypeScript Projects Course 2025
- 100 Hours Web Development Bootcamp - Build 23 React Projects
- React JS Masterclass: Zero To Job Ready With 10 Projects
- Big React JS Course With AI (Redux / Router / Tailwind CSS)
- React JS + ChatGPT Crash Course: Build Dynamic React Apps
- Advanced React: Design System, Design Patterns, Performance
- Microfrontends with React: A Complete Developer's Guide
Thanks!