Skip to content

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     | false

NoSQL 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: false

Key differences:

SQLNoSQL (Firestore)
Tables with rowsCollections with documents
Fixed schemaFlexible schema
Relationships via foreign keysNested documents or references
Joins for complex queriesDenormalize or use subcollections
Great for complex relationshipsGreat 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 β†’ Fields

Example: 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: Timestamp

Each 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:

OperationFirestore FunctionWhat It Does
CreateaddDoc()Adds a new document to a collection
ReadgetDocs(), getDoc()Fetches documents from a collection
UpdateupdateDoc()Modifies fields in an existing document
DeletedeleteDoc()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:

typescript
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 ID
  • title β€” The core data (what the task is)
  • completed β€” The status we need to track
  • userId β€” 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:

typescript
// 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:

typescript
// 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 value
  • orderBy() β€” Sort results by a field
  • limit() β€” Limit number of results

Examples:

typescript
// 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.

typescript
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 created
  • updatedAt β€” When it was last modified
  • completedAt β€” 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():

typescript
// 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:

typescript
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:

typescript
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) ​

typescript
// 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) ​

typescript
// 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) ​

typescript
// 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) ​

typescript
// 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) ​

typescript
// 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 β€” userId links 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:

Step 1: Understanding Databases β†’

Built for learning | Open source on GitHub