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
-
Cloud Firestore Documentation β
Official Firestore guides and API reference
-
Firestore Queries Guide β
Learn advanced querying techniques
-
Firestore Data Model β
Best practices for structuring your data