Core Concepts: Protected Feature (CRUD) β
Understanding databases, Firestore, and CRUD operations
Before we build our protected feature, let's understand the key concepts behind databases and data management. This isn't just about storing information β it's about building a system where each user has their own private data that persists over time.
What is a Database? β
A database is a structured collection of data that's stored on a server and persists even when your app closes. Think of it as a filing cabinet for your application.
Without a database, all your app's data lives in JavaScript variables. When you refresh the page, everything vanishes. With a database, data lives on a server somewhere, and your app fetches it when needed.
Why apps need databases:
- Persistence β Data survives page refreshes and browser closes
- Sharing β Multiple users can access and modify the same data
- Structure β Data is organized logically, not scattered
- Security β You control who can read/write what data
Imagine a to-do list app without a database: you add tasks, refresh the page, and everything's gone. With a database, those tasks stay saved until you delete them.
What is Firestore? β
Firestore (Cloud Firestore) is Google's NoSQL database for web and mobile apps. It's part of Firebase and works seamlessly with Firebase Authentication.
NoSQL vs SQL β
There are two main types of databases:
SQL databases (like MySQL, PostgreSQL) organize data in tables with rows and columns:
users table:
id | name | email
---+------------+------------------
1 | Alice | alice@example.com
2 | Bob | bob@example.com
todos table:
id | userId | title | completed
---+--------+-----------------+----------
1 | 1 | Buy groceries | false
2 | 1 | Walk dog | true
3 | 2 | Study React | falseNoSQL databases (like Firestore, MongoDB) organize data as documents and collections:
users/
alice123/
name: "Alice"
email: "alice@example.com"
todos/
todo1/
title: "Buy groceries"
completed: false
todo2/
title: "Walk dog"
completed: true
bob456/
name: "Bob"
email: "bob@example.com"
todos/
todo1/
title: "Study React"
completed: falseKey differences:
| SQL | NoSQL (Firestore) |
|---|---|
| Tables with rows | Collections with documents |
| Fixed schema | Flexible schema |
| Relationships via foreign keys | Nested documents or references |
| Joins for complex queries | Denormalize or use subcollections |
| Great for complex relationships | Great for hierarchical data |
We use Firestore because:
- β Integrates perfectly with Firebase Auth
- β Real-time updates out of the box
- β Scales automatically
- β Simple JavaScript API
- β Built-in security rules
- β No server setup required
Firestore Structure: Collections and Documents β
Firestore organizes data in collections and documents:
Collection β A group of documents. Like a folder holding files. Collections don't store data themselves; they just contain documents.
Document β A single record. Like a JSON object with fields. Documents store actual data.
Subcollection β A collection inside a document. Used for nested data.
Here's the pattern:
Collection β Document β Fields
Collection β Document β Subcollection β Document β FieldsExample: Todo app structure
todos (collection)
βββ todo1 (document)
β βββ title: "Buy groceries"
β βββ completed: false
β βββ userId: "abc123"
β βββ createdAt: Timestamp
βββ todo2 (document)
β βββ title: "Walk dog"
β βββ completed: true
β βββ userId: "abc123"
β βββ createdAt: Timestamp
βββ todo3 (document)
βββ title: "Study React"
βββ completed: false
βββ userId: "def456"
βββ createdAt: TimestampEach document has:
- A unique ID (auto-generated or custom)
- Fields (key-value pairs, like
title: "Buy groceries") - Optionally, subcollections (nested data)
Visual Diagram: Firestore Data Structure β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Firestore Database β
β β
β βββββββββββββββββββββββββββββββββββββββββββββββββ β
β β todos (collection) β β
β β β β
β β βββββββββββββββββββββββββββββββββββββββββββ β β
β β β todoABC123 (document) β β β
β β β βββββββββββββββββββββββββββββββββββββββ β β β
β β β β title: "Buy groceries" β β β β
β β β β completed: false β β β β
β β β β userId: "user1" β β β β
β β β β createdAt: Timestamp β β β β
β β β βββββββββββββββββββββββββββββββββββββββ β β β
β β βββββββββββββββββββββββββββββββββββββββββββ β β
β β β β
β β βββββββββββββββββββββββββββββββββββββββββββ β β
β β β todoXYZ789 (document) β β β
β β β βββββββββββββββββββββββββββββββββββββββ β β β
β β β β title: "Walk dog" β β β β
β β β β completed: true β β β β
β β β β userId: "user1" β β β β
β β β β createdAt: Timestamp β β β β
β β β βββββββββββββββββββββββββββββββββββββββ β β β
β β βββββββββββββββββββββββββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββWhat is CRUD? β
CRUD stands for the four basic operations you can perform on data:
- Create β Add new data
- Read β Retrieve existing data
- Update β Modify existing data
- Delete β Remove data
Every database-driven app is built on these operations. A to-do list app:
- Create β Add a new task
- Read β Show all tasks
- Update β Mark a task as complete
- Delete β Remove a task
In Firestore, these operations map to specific functions:
| Operation | Firestore Function | What It Does |
|---|---|---|
| Create | addDoc() | Adds a new document to a collection |
| Read | getDocs(), getDoc() | Fetches documents from a collection |
| Update | updateDoc() | Modifies fields in an existing document |
| Delete | deleteDoc() | Removes a document from a collection |
CRUD Flow Diagram β
User Action Firestore Operation Database
β β β
ββ Click "Add Task" βββββββΌββΊ addDoc() ββββββββββββΊβ
β β β
β β [New document
β β created]
β β β
ββββββββββββββββββββββ Success ββββββββββββββββββββ€
β β β
β β β
ββ View tasks βββββββββββββΌββΊ getDocs() βββββββββββΊβ
β β β
β β [Fetch all
β β documents]
β β β
ββββββββββββββββββββββ [Data] βββββββββββββββββββββ€
β β β
β β β
ββ Mark complete ββββββββββΌββΊ updateDoc() βββββββββΊβ
β β β
β β [Update
β β completed: true]
β β β
ββββββββββββββββββββββ Success ββββββββββββββββββββ€
β β β
β β β
ββ Delete task ββββββββββββΌββΊ deleteDoc() βββββββββΊβ
β β
β [Document
β removed]
β β
Success βββββββββββββββββββββWhat is Data Modeling? β
Data modeling is deciding how to structure your data. For our to-do app, we need to answer:
- What fields does each todo have?
- What data types should we use?
- How do we connect todos to users?
- What information is required vs optional?
Example data model for a todo:
interface Todo {
id: string; // Unique identifier (auto-generated)
title: string; // What the task is
completed: boolean; // Is it done?
userId: string; // Who owns this task?
createdAt: Timestamp; // When was it created?
description?: string; // Optional details
}Why this structure?
idβ Every document needs a unique IDtitleβ The core data (what the task is)completedβ The status we need to trackuserIdβ Links the task to its owner (crucial for privacy!)createdAtβ Helpful for sorting and displaying "when"descriptionβ Optional field (marked with?in TypeScript)
Good data modeling:
- β Include all necessary fields
- β Use appropriate data types (string, boolean, number, Timestamp)
- β Link data to users for privacy
- β Add timestamps for sorting/auditing
- β Keep it simple (don't over-engineer)
Bad data modeling:
- β Missing critical fields (like
userId) - β Wrong data types (storing numbers as strings)
- β Too much nesting (hard to query)
- β Storing redundant data everywhere
What are Firestore Queries? β
Queries let you filter and sort data instead of fetching everything.
Without queries, you'd fetch all todos from all users, then filter in JavaScript:
// Bad: Fetch everything, filter in JS
const allTodos = await getDocs(collection(db, 'todos'));
const myTodos = allTodos.filter(todo => todo.userId === currentUser.uid);With queries, Firestore filters server-side and only sends what you need:
// Good: Fetch only your todos
const q = query(
collection(db, 'todos'),
where('userId', '==', currentUser.uid)
);
const myTodos = await getDocs(q);Common query operations:
where()β Filter documents by field valueorderBy()β Sort results by a fieldlimit()β Limit number of results
Examples:
// Get only incomplete tasks
where('completed', '==', false)
// Get only my tasks
where('userId', '==', currentUser.uid)
// Sort by creation date (newest first)
orderBy('createdAt', 'desc')
// Combine: My incomplete tasks, sorted by date
query(
collection(db, 'todos'),
where('userId', '==', currentUser.uid),
where('completed', '==', false),
orderBy('createdAt', 'desc')
)Why queries matter:
- Performance β Don't fetch data you don't need
- Security β Combined with rules, users can't see others' data
- UX β Fast, filtered results
What are Timestamps? β
Timestamps are Firestore's way of storing dates and times. They're critical for sorting and showing "when" something happened.
import { Timestamp } from 'firebase/firestore';
// Create a timestamp (current time)
const now = Timestamp.now();
// Use in a document
await addDoc(collection(db, 'todos'), {
title: 'Buy groceries',
createdAt: Timestamp.now(),
});
// Convert to JavaScript Date for display
const date = timestamp.toDate();
console.log(date.toLocaleDateString()); // "1/15/2024"Why use Firestore Timestamps instead of JavaScript Dates?
- β Server-side timestamps (accurate across timezones)
- β Consistent format (no timezone issues)
- β Sortable in queries
- β Firestore understands them natively
Common uses:
createdAtβ When the item was createdupdatedAtβ When it was last modifiedcompletedAtβ When a task was finished
What is Real-Time Data? β
Real-time data means your UI updates automatically when the database changes, without refreshing the page.
Firestore supports real-time updates with onSnapshot():
// Regular read: fetch once
const todos = await getDocs(collection(db, 'todos'));
// Real-time: listen for changes
onSnapshot(collection(db, 'todos'), (snapshot) => {
const todos = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
setTodos(todos); // Update state automatically
});How it works:
Your App Firestore
β β
βββΊ onSnapshot() ββββββ€
β β
β [Listening...] β
β β
β β [Another user adds a todo]
β β
ββββ Update βββββββββββ€
β β
β [UI re-renders] βBenefits:
- β No manual refreshing
- β Multi-user apps stay in sync
- β Feels instant and responsive
Downsides:
- β More complex to manage
- β More Firestore reads (can cost more)
- β Need to handle cleanup
For this bootcamp, we'll use regular reads (fetch on page load) to keep it simple. Real-time is a powerful feature you can add later.
What are Loading States? β
Loading states tell users "the app is working, please wait." Without them, users wonder if their click worked.
When fetching data from Firestore, there's a delay (network request). During that time, show a loading indicator:
function TodoList() {
const [todos, setTodos] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchTodos = async () => {
setLoading(true);
const snapshot = await getDocs(collection(db, 'todos'));
const data = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
setTodos(data);
setLoading(false);
};
fetchTodos();
}, []);
if (loading) return <p>Loading...</p>;
return (
<ul>
{todos.map(todo => <li key={todo.id}>{todo.title}</li>)}
</ul>
);
}Loading states prevent:
- β Showing stale data
- β Rendering broken UI (like empty lists)
- β Users clicking buttons multiple times
Best practices:
- Show loading indicator during async operations
- Disable submit buttons while processing
- Clear loading state after success OR error
- Show meaningful messages ("Loading tasks..." not "Loading...")
What are Empty States? β
Empty states tell users what to do when there's no data.
Without an empty state:
ββββββββββββββββββββββββ
β My Tasks β
β β
β β β User sees nothing, confused
β β
ββββββββββββββββββββββββWith an empty state:
ββββββββββββββββββββββββ
β My Tasks β
β β
β No tasks yet! β
β [Add your first] β β Clear message + action
β β
ββββββββββββββββββββββββExample implementation:
function TodoList({ todos }) {
if (todos.length === 0) {
return (
<div>
<p>No tasks yet!</p>
<Link to="/todos/new">Add your first task</Link>
</div>
);
}
return (
<ul>
{todos.map(todo => <li key={todo.id}>{todo.title}</li>)}
</ul>
);
}Why empty states matter:
- β Guide users on what to do next
- β Confirm the app is working (not broken)
- β Reduce confusion
- β Improve first-time user experience
How It All Fits Together: Complete CRUD Example β
Let's walk through a complete example: building a to-do list with CRUD operations.
1. Data Model (TypeScript Interface) β
// src/types/todo.ts
import { Timestamp } from 'firebase/firestore';
export interface Todo {
id: string;
title: string;
completed: boolean;
userId: string;
createdAt: Timestamp;
}2. Create (Add New Todo) β
// src/components/AddTodoForm.tsx
import { addDoc, collection, Timestamp } from 'firebase/firestore';
import { db } from '../lib/firebase';
import { useAuth } from '../contexts/AuthContext';
function AddTodoForm() {
const [title, setTitle] = useState('');
const { currentUser } = useAuth();
const handleSubmit = async (e) => {
e.preventDefault();
await addDoc(collection(db, 'todos'), {
title,
completed: false,
userId: currentUser.uid,
createdAt: Timestamp.now(),
});
setTitle(''); // Clear form
};
return (
<form onSubmit={handleSubmit}>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
<button type="submit">Add</button>
</form>
);
}3. Read (Fetch Todos) β
// src/pages/TodosPage.tsx
import { collection, query, where, getDocs, orderBy } from 'firebase/firestore';
import { db } from '../lib/firebase';
import { useAuth } from '../contexts/AuthContext';
function TodosPage() {
const [todos, setTodos] = useState([]);
const [loading, setLoading] = useState(true);
const { currentUser } = useAuth();
useEffect(() => {
const fetchTodos = async () => {
setLoading(true);
// Query: only my todos, sorted by creation date
const q = query(
collection(db, 'todos'),
where('userId', '==', currentUser.uid),
orderBy('createdAt', 'desc')
);
const snapshot = await getDocs(q);
const data = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
setTodos(data);
setLoading(false);
};
fetchTodos();
}, [currentUser]);
if (loading) return <p>Loading tasks...</p>;
if (todos.length === 0) {
return (
<div>
<p>No tasks yet!</p>
<Link to="/todos/new">Add your first task</Link>
</div>
);
}
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}4. Update (Mark as Complete) β
// src/components/TodoItem.tsx
import { doc, updateDoc } from 'firebase/firestore';
import { db } from '../lib/firebase';
function TodoItem({ todo }) {
const toggleComplete = async () => {
const todoRef = doc(db, 'todos', todo.id);
await updateDoc(todoRef, {
completed: !todo.completed,
});
};
return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={toggleComplete}
/>
{todo.title}
</li>
);
}5. Delete (Remove Todo) β
// src/components/TodoItem.tsx
import { doc, deleteDoc } from 'firebase/firestore';
import { db } from '../lib/firebase';
function TodoItem({ todo }) {
const handleDelete = async () => {
if (!confirm('Delete this task?')) return;
const todoRef = doc(db, 'todos', todo.id);
await deleteDoc(todoRef);
};
return (
<li>
{todo.title}
<button onClick={handleDelete}>Delete</button>
</li>
);
}Data Flow Diagram: User Action to UI Update β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β User Action: "Add new task" β
ββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββ
β Form submission β
β (handleSubmit) β
ββββββββββββ¬ββββββββββββ
β
βΌ
ββββββββββββββββββββββββ
β addDoc() β
β (Firestore Create) β
ββββββββββββ¬ββββββββββββ
β
βΌ
ββββββββββββββββββββββββ
β Firestore Database β
β [New document saved] β
ββββββββββββ¬ββββββββββββ
β
βΌ
ββββββββββββββββββββββββ
β Success response β
ββββββββββββ¬ββββββββββββ
β
βΌ
ββββββββββββββββββββββββ
β Re-fetch todos β
β (or optimistic UI) β
ββββββββββββ¬ββββββββββββ
β
βΌ
ββββββββββββββββββββββββ
β Update state β
β setTodos([...]) β
ββββββββββββ¬ββββββββββββ
β
βΌ
ββββββββββββββββββββββββ
β React re-renders β
β UI shows new task β
ββββββββββββββββββββββββVisual Diagram: CRUD Operations β
CREATE READ UPDATE DELETE
β β β β
βΌ βΌ βΌ βΌ
βββββββββββ βββββββββββ βββββββββββ βββββββββββ
β addDoc()β βgetDocs()β βupdateDocβ βdeleteDocβ
ββββββ¬βββββ ββββββ¬βββββ ββββββ¬βββββ ββββββ¬βββββ
β β β β
βΌ βΌ βΌ βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Firestore Database β
β β
β todos (collection) β
β βββ todo1 { title: "Buy groceries", completed: false } β
β βββ todo2 { title: "Walk dog", completed: true } β
β βββ todo3 { title: "Study React", completed: false } β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β β β
βββββββββββββββββββ΄βββββββββββββββββββββ΄βββββββββββββββββββββββ
β
βΌ
βββββββββββββββ
β React State β
β (todos) β
ββββββββ¬βββββββ
β
βΌ
βββββββββββββββ
β UI β
β (Todo List) β
βββββββββββββββWhy These Patterns Matter β
CRUD operations are the foundation of data-driven apps. Once you understand Create, Read, Update, and Delete, you can build:
- Social media (posts, comments, likes)
- E-commerce (products, cart, orders)
- Note-taking apps
- Project management tools
- Basically any app that stores user data
Firestore makes these operations safe and scalable:
- Authentication integration β
userIdlinks data to users - Security rules β Control who can read/write
- Real-time capabilities β Optional instant updates
- Offline support β Works without internet (advanced)
React + Firestore is a powerful combination:
- React handles UI and state
- Firestore handles data persistence
- Firebase Auth handles user identity
- Security rules handle permissions
What You'll Build β
In this slice, you'll create:
- β Firestore integration with your app
- β TypeScript types for your data model
- β A form to add new items (Create)
- β A list view with loading states (Read)
- β Edit functionality for existing items (Update)
- β Delete with confirmation (Delete)
- β Empty states when there's no data
- β User-specific data (each user sees only their items)
By the end, you'll have a fully functional CRUD feature that persists data to Firestore and keeps each user's data private.
Next Steps β
Now that you understand the concepts, let's start building: