TechLead
Lesson 3 of 8
6 min read
Firebase

Cloud Firestore

Learn to use Cloud Firestore, Firebase's flexible and scalable NoSQL database

What is Cloud Firestore?

Cloud Firestore is Firebase's flexible, scalable NoSQL cloud database that stores and syncs data for client and server-side development. It keeps your data in sync across client apps through real-time listeners and offers offline support so your app works even when disconnected.

Unlike traditional relational databases, Firestore organizes data into documents and collections, making it intuitive to work with and allowing for flexible data structures.

πŸ“š Firestore Data Model

  • πŸ“„ Documents: Lightweight records containing fields with values (like JSON objects).
  • πŸ“ Collections: Containers for documents. Collections can only contain documents.
  • πŸ”— Subcollections: Collections within documents, allowing nested data structures.
  • πŸ” References: Pointers to documents or collections in your database.

Setting Up Firestore

Initialize Firestore in your application:

import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';

const firebaseConfig = {
  // Your config here
};

const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

export { db };

Adding Documents

There are several ways to add documents to Firestore:

Add with Auto-generated ID

import { collection, addDoc } from 'firebase/firestore';

async function addUser(userData) {
  try {
    const docRef = await addDoc(collection(db, 'users'), {
      name: userData.name,
      email: userData.email,
      createdAt: new Date(),
      role: 'user'
    });
    console.log('Document written with ID:', docRef.id);
    return docRef.id;
  } catch (error) {
    console.error('Error adding document:', error);
    throw error;
  }
}

Set with Custom ID

import { doc, setDoc } from 'firebase/firestore';

async function createUserProfile(userId, profileData) {
  try {
    await setDoc(doc(db, 'users', userId), {
      name: profileData.name,
      email: profileData.email,
      bio: profileData.bio,
      updatedAt: new Date()
    });
    console.log('Profile created for user:', userId);
  } catch (error) {
    console.error('Error creating profile:', error);
  }
}

// Merge with existing data instead of overwriting
await setDoc(doc(db, 'users', userId), {
  lastLogin: new Date()
}, { merge: true });

Reading Documents

Retrieve single documents or entire collections:

Get a Single Document

import { doc, getDoc } from 'firebase/firestore';

async function getUser(userId) {
  const docRef = doc(db, 'users', userId);
  const docSnap = await getDoc(docRef);

  if (docSnap.exists()) {
    console.log('User data:', docSnap.data());
    return { id: docSnap.id, ...docSnap.data() };
  } else {
    console.log('No such document!');
    return null;
  }
}

Get All Documents in a Collection

import { collection, getDocs } from 'firebase/firestore';

async function getAllUsers() {
  const querySnapshot = await getDocs(collection(db, 'users'));
  const users = [];

  querySnapshot.forEach((doc) => {
    users.push({ id: doc.id, ...doc.data() });
  });

  return users;
}

Querying Documents

Filter and sort documents with queries:

import {
  collection,
  query,
  where,
  orderBy,
  limit,
  getDocs
} from 'firebase/firestore';

// Simple query
async function getActiveUsers() {
  const q = query(
    collection(db, 'users'),
    where('status', '==', 'active')
  );

  const querySnapshot = await getDocs(q);
  return querySnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
}

// Complex query with multiple conditions
async function searchUsers(role, minAge) {
  const q = query(
    collection(db, 'users'),
    where('role', '==', role),
    where('age', '>=', minAge),
    orderBy('age', 'asc'),
    limit(10)
  );

  const querySnapshot = await getDocs(q);
  return querySnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
}

// Query operators: ==, !=, <, <=, >, >=, array-contains,
// array-contains-any, in, not-in

Real-time Updates

Listen for live data changes:

import { doc, onSnapshot, collection } from 'firebase/firestore';

// Listen to a single document
function subscribeToUser(userId, callback) {
  const unsubscribe = onSnapshot(doc(db, 'users', userId), (doc) => {
    if (doc.exists()) {
      callback({ id: doc.id, ...doc.data() });
    }
  });

  // Return unsubscribe function to stop listening
  return unsubscribe;
}

// Listen to a collection
function subscribeToUsers(callback) {
  const q = query(collection(db, 'users'), where('status', '==', 'active'));

  const unsubscribe = onSnapshot(q, (querySnapshot) => {
    const users = [];
    querySnapshot.forEach((doc) => {
      users.push({ id: doc.id, ...doc.data() });
    });
    callback(users);
  });

  return unsubscribe;
}

// Usage in React
useEffect(() => {
  const unsubscribe = subscribeToUsers((users) => {
    setUsers(users);
  });

  return () => unsubscribe(); // Cleanup on unmount
}, []);

Updating Documents

Modify existing documents:

import { doc, updateDoc, arrayUnion, arrayRemove, increment } from 'firebase/firestore';

// Simple update
async function updateUser(userId, data) {
  const userRef = doc(db, 'users', userId);
  await updateDoc(userRef, {
    name: data.name,
    updatedAt: new Date()
  });
}

// Update specific fields
async function updateUserEmail(userId, newEmail) {
  const userRef = doc(db, 'users', userId);
  await updateDoc(userRef, {
    email: newEmail
  });
}

// Add to an array
async function addTag(userId, tag) {
  const userRef = doc(db, 'users', userId);
  await updateDoc(userRef, {
    tags: arrayUnion(tag)
  });
}

// Remove from an array
async function removeTag(userId, tag) {
  const userRef = doc(db, 'users', userId);
  await updateDoc(userRef, {
    tags: arrayRemove(tag)
  });
}

// Increment a number
async function incrementScore(userId, points) {
  const userRef = doc(db, 'users', userId);
  await updateDoc(userRef, {
    score: increment(points)
  });
}

Deleting Documents

Remove documents and fields:

import { doc, deleteDoc, updateDoc, deleteField } from 'firebase/firestore';

// Delete a document
async function deleteUser(userId) {
  await deleteDoc(doc(db, 'users', userId));
  console.log('User deleted');
}

// Delete a specific field
async function removeUserBio(userId) {
  const userRef = doc(db, 'users', userId);
  await updateDoc(userRef, {
    bio: deleteField()
  });
}

Subcollections

Organize data with nested collections:

import { collection, doc, addDoc, getDocs } from 'firebase/firestore';

// Add a document to a subcollection
async function addPost(userId, postData) {
  const postsRef = collection(db, 'users', userId, 'posts');
  const docRef = await addDoc(postsRef, {
    title: postData.title,
    content: postData.content,
    createdAt: new Date()
  });
  return docRef.id;
}

// Get all documents from a subcollection
async function getUserPosts(userId) {
  const postsRef = collection(db, 'users', userId, 'posts');
  const querySnapshot = await getDocs(postsRef);

  return querySnapshot.docs.map(doc => ({
    id: doc.id,
    ...doc.data()
  }));
}

// Reference a specific document in a subcollection
const postRef = doc(db, 'users', userId, 'posts', postId);

Batch Writes & Transactions

Perform multiple operations atomically:

import { writeBatch, runTransaction, doc } from 'firebase/firestore';

// Batch writes - up to 500 operations
async function batchUpdate() {
  const batch = writeBatch(db);

  const user1Ref = doc(db, 'users', 'user1');
  const user2Ref = doc(db, 'users', 'user2');

  batch.update(user1Ref, { status: 'inactive' });
  batch.update(user2Ref, { status: 'inactive' });
  batch.delete(doc(db, 'users', 'user3'));

  await batch.commit();
  console.log('Batch completed');
}

// Transactions - for read-then-write operations
async function transferPoints(fromId, toId, points) {
  await runTransaction(db, async (transaction) => {
    const fromRef = doc(db, 'users', fromId);
    const toRef = doc(db, 'users', toId);

    const fromDoc = await transaction.get(fromRef);
    const toDoc = await transaction.get(toRef);

    if (!fromDoc.exists() || !toDoc.exists()) {
      throw new Error('User not found');
    }

    const fromPoints = fromDoc.data().points;
    if (fromPoints < points) {
      throw new Error('Insufficient points');
    }

    transaction.update(fromRef, { points: fromPoints - points });
    transaction.update(toRef, { points: toDoc.data().points + points });
  });
}

Data Types in Firestore

Firestore supports various data types:

import { Timestamp, GeoPoint, serverTimestamp } from 'firebase/firestore';

const document = {
  // Primitive types
  name: 'John Doe',           // string
  age: 30,                     // number
  isActive: true,              // boolean
  nullable: null,              // null

  // Complex types
  tags: ['developer', 'designer'],  // array
  address: {                         // map (nested object)
    city: 'San Francisco',
    country: 'USA'
  },

  // Special Firestore types
  createdAt: serverTimestamp(),           // Server timestamp
  birthday: Timestamp.fromDate(new Date('1990-01-15')),  // Timestamp
  location: new GeoPoint(37.7749, -122.4194),  // GeoPoint

  // Reference to another document
  teamRef: doc(db, 'teams', 'team123')
};

πŸ’‘ Key Takeaways

  • β€’ Firestore organizes data into collections and documents
  • β€’ Use addDoc for auto-generated IDs, setDoc for custom IDs
  • β€’ Real-time listeners with onSnapshot keep data synchronized
  • β€’ Query documents with where, orderBy, and limit
  • β€’ Use transactions for read-then-write operations
  • β€’ Subcollections help organize hierarchical data

πŸ“š Learn More

Continue Learning