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 MERN Stack, React.js, or React Native, you can take the following cheap course:
- Mastering React JS
- Master React Native Animations
- React: React Native Mobile Development: 3-in-1
- MERN Stack Front To Back: Full Stack React, Redux & Node. js
- Learning The React Native Development
Thanks!