Firestore
Common use cases
Core
Initialization
import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
const firebaseApp = initializeApp({ /* config */});
const db = getFirestore(firebaseApp);
Connect to the Emulator Suite
import { getFirestore, useFirestoreEmulator } from "firebase/firestore";
// firebaseApps previously initialized using initializeApp()
const db = getFirestore();
useFirestoreEmulator(db, 'localhost', 8080);
Disable network
import { disableNetwork } from "firebase/firestore";
await disableNetwork(db);
console.log("Network disabled!");
// Do offline actions
// [START_EXCLUDE]
console.log("Network disabled!");
// [END_EXCLUDE]
Enable offline (IndexedDB persistence)
import { enableIndexedDbPersistence } from "firebase/firestore";
enableIndexedDbPersistence(db)
.catch((err) => {
if (err.code == 'failed-precondition') {
// Multiple tabs open, persistence can only be enabled
// in one tab at a a time.
// ...
} else if (err.code == 'unimplemented') {
// The current browser does not support all of the
// features required to enable persistence
// ...
}
});
// Subsequent queries will use persistence, if it was enabled successfully
Realtime listeners
Listen to a document
import { doc, onSnapshot } from "firebase/firestore";
const unsub = onSnapshot(doc(db, "cities", "SF"), (doc) => {
console.log("Current data: ", doc.data());
});
Query a collection
import { collection, query, where, onSnapshot } from "firebase/firestore";
const q = query(collection(db, "cities"), where("state", "==", "CA"));
const unsubscribe = onSnapshot(q, (querySnapshot) => {
const cities = [];
querySnapshot.forEach((doc) => {
cities.push(doc.data().name);
});
console.log("Current cities in CA: ", cities.join(", "));
});
Range query
import { collection, where, query, onSnapshot } from "firebase/firestore";
const q = query(collection(db, "users"), where("born", "<", 1900));
const unsubscribe = onSnapshot(q, (snapshot) => {
console.log("Current users born before 1900:");
snapshot.forEach((userSnapshot) => {
console.log(userSnapshot.data());
});
});
Group by query
import { collectionGroup, query, where, getDocs } from "firebase/firestore";
const museums = query(collectionGroup(db, 'landmarks'), where('type', '==', 'museum'));
const querySnapshot = await getDocs(museums);
querySnapshot.forEach((doc) => {
console.log(doc.id, ' => ', doc.data());
});
In filter
import { query, where } from "firebase/firestore";
const q = query(citiesRef, where('country', 'in', ['USA', 'Japan']));
Not in filter
import { query, where } from "firebase/firestore";
const q = query(citiesRef, where('country', 'not-in', ['USA', 'Japan']));
Listen for errors
import { collection, onSnapshot } from "firebase/firestore";
const unsubscribe = onSnapshot(
collection(db, "cities"),
(snapshot) => {
// ...
},
(error) => {
// ...
});
Detach listener
import { collection, onSnapshot } from "firebase/firestore";
const unsubscribe = onSnapshot(collection(db, "cities"), () => {
// Respond to data
// ...
});
// Later ...
// Stop listening to changes
unsubscribe();
Include cache updates
import { collection, onSnapshot, where, query } from "firebase/firestore";
const q = query(collection(db, "cities"), where("state", "==", "CA"));
onSnapshot(q, { includeMetadataChanges: true }, (snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === "added") {
console.log("New city: ", change.doc.data());
}
const source = snapshot.metadata.fromCache ? "local cache" : "server";
console.log("Data came from " + source);
});
});
Listen for local updates
import { doc, onSnapshot } from "firebase/firestore";
const unsub = onSnapshot(doc(db, "cities", "SF"), (doc) => {
const source = doc.metadata.hasPendingWrites ? "Local" : "Server";
console.log(source, " data: ", doc.data());
});
CRUD operations
Get a document
import { doc, getDoc } from "firebase/firestore";
const docRef = doc(db, "cities", "SF");
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
console.log("Document data:", docSnap.data());
} else {
// doc.data() will be undefined in this case
console.log("No such document!");
}
Get a collection
import { collection, getDocs } from "firebase/firestore";
const querySnapshot = await getDocs(collection(db, "users"));
querySnapshot.forEach((doc) => {
console.log(`${doc.id} => ${doc.data()}`);
});
Get a customer object (Converter)
import { doc, getDoc} from "firebase/firestore";
const ref = doc(db, "cities", "LA").withConverter(cityConverter);
const docSnap = await getDoc(ref);
if (docSnap.exists()) {
// Convert to City object
const city = docSnap.data();
// Use a City instance method
console.log(city.toString());
} else {
console.log("No such document!");
}
Query a collection
import { collection, query, where, getDocs } from "firebase/firestore";
const q = query(collection(db, "cities"), where("capital", "==", true));
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
// doc.data() is never undefined for query doc snapshots
console.log(doc.id, " => ", doc.data());
});
Query a collection with multiple clauses
// Will return all Springfields
import { collection, query, orderBy, startAt } from "firebase/firestore";
const q1 = query(collection(db, "cities"),
orderBy("name"),
orderBy("state"),
startAt("Springfield"));
// Will return "Springfield, Missouri" and "Springfield, Wisconsin"
const q2 = query(collection(db, "cities"),
orderBy("name"),
orderBy("state"),
startAt("Springfield", "Missouri"));
Collection group query (Query across collections)
import { collectionGroup, query, where, getDocs } from "firebase/firestore";
const museums = query(collectionGroup(db, 'landmarks'), where('type', '==', 'museum'));
const querySnapshot = await getDocs(museums);
querySnapshot.forEach((doc) => {
console.log(doc.id, ' => ', doc.data());
});
Set a document
import { collection, doc, setDoc } from "firebase/firestore";
// Add a new document with a generated id
const newCityRef = doc(collection(db, "cities"));
// later...
await setDoc(newCityRef, data);
Set a document with merge
import { doc, setDoc } from "firebase/firestore";
const cityRef = doc(db, 'cities', 'BJ');
setDoc(cityRef, { capital: true }, { merge: true });
Update a document
import { doc, updateDoc } from "firebase/firestore";
const washingtonRef = doc(db, "cities", "DC");
// Set the "capital" field of the city 'DC'
await updateDoc(washingtonRef, {
capital: true
});
Update a document with an array
import { doc, updateDoc, arrayUnion, arrayRemove } from "firebase/firestore";
const washingtonRef = doc(db, "cities", "DC");
// Atomically add a new region to the "regions" array field.
await updateDoc(washingtonRef, {
regions: arrayUnion("greater_virginia")
});
// Atomically remove a region from the "regions" array field.
await updateDoc(washingtonRef, {
regions: arrayRemove("east_coast")
});
Update a document with an increment
import { doc, updateDoc, increment } from "firebase/firestore";
const washingtonRef = doc(db, "cities", "DC");
// Atomically increment the population of the city by 50.
await updateDoc(washingtonRef, {
population: increment(50)
});
Update a document with a nested object
import { doc, setDoc, updateDoc } from "firebase/firestore";
// Create an initial document to update.
const frankDocRef = doc(db, "users", "frank");
await setDoc(frankDocRef, {
name: "Frank",
favorites: { food: "Pizza", color: "Blue", subject: "recess" },
age: 12
});
// To update age and favorite color:
await updateDoc(frankDocRef, {
"age": 13,
"favorites.color": "Red"
});
Update a document with a server timestamp
import { updateDoc, serverTimestamp } from "firebase/firestore";
const docRef = doc(db, 'objects', 'some-id');
// Update the timestamp field with the value from the server
const updateTimestamp = await updateDoc(docRef, {
timestamp: serverTimestamp()
});
Set a document with a data types
import { doc, setDoc, Timestamp } from "firebase/firestore";
const docData = {
stringExample: "Hello world!",
booleanExample: true,
numberExample: 3.14159265,
dateExample: Timestamp.fromDate(new Date("December 10, 1815")),
arrayExample: [5, true, "hello"],
nullExample: null,
objectExample: {
a: 5,
b: {
nested: "foo"
}
}
};
await setDoc(doc(db, "data", "one"), docData);
Delete a document
import { doc, deleteDoc } from "firebase/firestore";
await deleteDoc(doc(db, "cities", "DC"));
Delete a collection
/**
* Delete a collection, in batches of batchSize. Note that this does
* not recursively delete subcollections of documents in the collection
*/
import { collection, query, orderBy, limit, getDocs, writeBatch } from "firebase/firestore";
function deleteCollection(db, collectionRef, batchSize) {
const q = query(collectionRef, orderBy('__name__'), limit(batchSize));
return new Promise((resolve) => {
deleteQueryBatch(db, q, batchSize, resolve);
});
}
async function deleteQueryBatch(db, query, batchSize, resolve) {
const snapshot = await getDocs(query);
// When there are no documents left, we are done
let numDeleted = 0;
if (snapshot.size > 0) {
// Delete documents in a batch
const batch = writeBatch(db);
snapshot.docs.forEach((doc) => {
batch.delete(doc.ref);
});
await batch.commit();
numDeleted = snapshot.size;
}
if (numDeleted < batchSize) {
resolve();
return;
}
// Recurse on the next process tick, to avoid
// exploding the stack.
setTimeout(() => {
deleteQueryBatch(db, query, batchSize, resolve);
}, 0);
}
Delete a field
import { doc, updateDoc, deleteField } from "firebase/firestore";
const cityRef = doc(db, 'cities', 'BJ');
// Remove the 'capital' field from the document
await updateDoc(cityRef, {
capital: deleteField()
});
Run a transaction
import { runTransaction } from "firebase/firestore";
try {
await runTransaction(db, async (transaction) => {
const sfDoc = await transaction.get(sfDocRef);
if (!sfDoc.exists()) {
throw "Document does not exist!";
}
const newPopulation = sfDoc.data().population + 1;
transaction.update(sfDocRef, { population: newPopulation });
});
console.log("Transaction successfully committed!");
} catch (e) {
console.log("Transaction failed: ", e);
}
Advanced
Pagination
import { collection, query, orderBy, startAfter, limit, getDocs } from "firebase/firestore";
// Query the first page of docs
const first = query(collection(db, "cities"), orderBy("population"), limit(25));
const documentSnapshots = await getDocs(first);
// Get the last visible document
const lastVisible = documentSnapshots.docs[documentSnapshots.docs.length-1];
console.log("last", lastVisible);
// Construct a new query starting at this document,
// get the next 25 cities.
const next = query(collection(db, "cities"),
orderBy("population"),
startAfter(lastVisible),
limit(25));
Advanced Transactions
import { collection, doc, runTransaction } from "firebase/firestore";
async function addRating(restaurantRef, rating) {
// Create a reference for a new rating, for use inside the transaction
const ratingRef = doc(collection(restaurantRef, 'ratings'));
// In a transaction, add the new rating and update the aggregate totals
await runTransaction(db, async (transaction) => {
const res = await transaction.get(restaurantRef);
if (!res.exists()) {
throw "Document does not exist!";
}
// Compute new number of ratings
const newNumRatings = res.data().numRatings + 1;
// Compute new average rating
const oldRatingTotal = res.data().avgRating * res.data().numRatings;
const newAvgRating = (oldRatingTotal + rating) / newNumRatings;
// Commit to Firestore
transaction.update(restaurantRef, {
numRatings: newNumRatings,
avgRating: newAvgRating
});
transaction.set(ratingRef, { rating: rating });
});
}
Geoquery — Add hash
import { doc, updateDoc } from 'firebase/firestore';
// Compute the GeoHash for a lat/lng point
const lat = 51.5074;
const lng = 0.1278;
const hash = geofire.geohashForLocation([lat, lng]);
// Add the hash and the lat/lng to the document. We will use the hash
// for queries and the lat/lng for distance comparisons.
const londonRef = doc(db, 'cities', 'LON');
await updateDoc(londonRef, {
geohash: hash,
lat: lat,
lng: lng
});
Geoquery — Query hashes
import { collection, query, orderBy, startAt, endAt, getDocs } from 'firebase/firestore';
// Find cities within 50km of London
const center = [51.5074, 0.1278];
const radiusInM = 50 * 1000;
// Each item in 'bounds' represents a startAt/endAt pair. We have to issue
// a separate query for each pair. There can be up to 9 pairs of bounds
// depending on overlap, but in most cases there are 4.
const bounds = geofire.geohashQueryBounds(center, radiusInM);
const promises = [];
for (const b of bounds) {
const q = query(
collection(db, 'cities'),
orderBy('geohash'),
startAt(b[0]),
endAt(b[1]));
promises.push(getDocs(q));
}
// Collect all the query results together into a single list
const snapshots = await Promise.all(promises);
const matchingDocs = [];
for (const snap of snapshots) {
for (const doc of snap.docs) {
const lat = doc.get('lat');
const lng = doc.get('lng');
// We have to filter out a few false positives due to GeoHash
// accuracy, but most will match
const distanceInKm = geofire.distanceBetween([lat, lng], center);
const distanceInM = distanceInKm * 1000;
if (distanceInM <= radiusInM) {
matchingDocs.push(doc);
}
}
}