Simplifying Firestore Interactions in React Apps with a Handy JavaScript Utility

Iqbal Shehzada
5 min readMar 26, 2024

--

In the ever-evolving landscape of web development, creating seamless and efficient database interactions is crucial for delivering a stellar user experience. For those of us leveraging Firebase Firestore in our React applications, the journey from concept to code can sometimes feel like navigating through a dense forest. However, the introduction of a new JavaScript utility aims to clear the path, making Firestore operations more accessible and less cumbersome.

The Genesis of the Utility

Born out of the necessity to streamline Firestore database operations, this utility encapsulates the common CRUD (Create, Read, Update, Delete) operations into a single, easy-to-use module. It abstracts away the boilerplate code required to interact with Firestore, allowing developers to perform database operations with minimal code. This not only improves the readability of your code but also significantly reduces development time.

Key Features and Operations

The utility provides a comprehensive suite of functions designed to cover most of the database operations you’ll need for your frontend applications. Let’s delve into some of its key features:

Adding Documents

Creating new documents in Firestore is simplified with the addDocument function. It takes a collection name and the data to be stored as arguments, returning the document ID upon success. This function is perfect for adding new records without worrying about generating unique IDs.

For cases where you want to specify a custom document ID, the addDocumentWithId function comes to the rescue. It allows for the creation of a document with a predefined ID, offering more control over document organization and retrieval.

Updating and Deleting Documents

Updating existing documents is straightforward with the updateDocument function. It requires the collection name, document ID, and the new data to update the document with. This function ensures that your data remains current and accurate.

Deleting documents is just as easy, using the deleteDocument function. By providing the collection name and the document ID, you can remove records from your database, keeping it clean and organized.

Fetching Documents

Retrieving documents from Firestore is a common operation, and this utility offers multiple ways to do so. The getDocumentById function fetches a single document using its ID, while getDocumentsByFilters allows for querying documents based on specific criteria. For fetching all documents in a collection, possibly sorted by a field, getAllDocuments has got you covered.

Miscellaneous Utilities

Besides CRUD operations, the utility includes a createRefFromString function for generating document references from a collection name and document ID. This is particularly useful for operations that require a document reference instead of a direct database query.

Code:

import { db } from "../../firebase";
import {
collection,
addDoc,
DocumentData,
deleteDoc,
doc,
updateDoc,
getDoc,
query,
where,
getDocs,
WhereFilterOp,
orderBy,
setDoc,
DocumentReference,
} from "firebase/firestore";

export const addDocument = async ({
collectionName,
data,
}: {
collectionName: string;
data: DocumentData;
}): Promise<string> => {
try {
const docRef = await addDoc(collection(db, collectionName), data);

console.log(
`Database Service: Document written with ID: [${docRef.id}] to Collection: [${collectionName}]`
);
return docRef.id;
} catch (error) {
console.error(`Database Service [addDocument] Error: ${error}`);
throw error;
}
};

export const addDocumentWithId = async ({
collectionName,
data,
customId,
}: {
collectionName: string;
data: DocumentData;
customId: string; // Add a parameter for the custom ID
}): Promise<string> => {
try {
const docRef = doc(collection(db, collectionName), customId);
await setDoc(docRef, data);

console.log(
`Database Service: Document written with custom ID: [${customId}] to Collection: [${collectionName}]`
);
return customId; // Return the custom ID instead of docRef.id
} catch (error) {
console.error(`Database Service [addDocumentWithId] Error: ${error}`);
throw error;
}
};

export const deleteDocument = async ({
collectionName,
documentId,
}: {
collectionName: string;
documentId: string;
}): Promise<void> => {
try {
await deleteDoc(doc(db, collectionName, documentId));

console.log(
`Database Service: Document with ID: [${documentId}] successfully deleted from Collection [${collectionName}]`
);
} catch (error) {
console.error(`Database Service [deleteDocument] Error: ${error}`);
throw error;
}
};

export const updateDocument = async ({
collectionName,
documentId,
data,
}: {
collectionName: string;
documentId: string;
data: Record<string, any>;
}): Promise<void> => {
try {
const documentRef = doc(db, collectionName, documentId);
await updateDoc(documentRef, data);
console.log(
`Database Service: Document with ID: [${documentId}] successfully updated in Collection [${collectionName}]`
);
} catch (error) {
console.error(`Database Service [updateDocument] Error ${error}`);
throw error;
}
};

export const getDocumentById = async ({
collectionName,
documentId,
}: {
collectionName: string;
documentId: string;
}): Promise<any> => {
try {
const documentRef = doc(db, collectionName, documentId);
const documentSnapshot = await getDoc(documentRef);

if (documentSnapshot.exists()) {
console.log(
`Database Service: Document with ID: [${documentId}] successfully retrieved from Collection [${collectionName}]`
);
return { id: documentSnapshot.id, ...documentSnapshot.data() };
} else {
console.log(
`Database Service: No document found with ID: [${documentId}] in Collection [${collectionName}]`
);
return null;
}
} catch (error) {
console.error(`Database Service [getDocumentById] Error ${error}`);
throw error;
}
};

export const getDocumentsByFilters = async ({
collectionName,
filters,
}: {
collectionName: string;
filters: Array<{
field: string;
operator: WhereFilterOp;
value: any;
}>;
}): Promise<{ documents: any[]; count: number }> => {
try {
const collectionRef = collection(db, collectionName);
const queryConstraints = filters.map((filter) =>
where(filter.field, filter.operator, filter.value)
);
const q = query(collectionRef, ...queryConstraints);

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

if (documents.length > 0) {
console.log(
`Database Service: Successfully retrieved ${documents.length} documents from Collection [${collectionName}]`
);
} else {
console.log(
`Database Service: No documents found in Collection [${collectionName}] with specified filters`
);
}

return { documents, count: querySnapshot.size };
} catch (error) {
console.error(`Database Service [getDocumentsByFilters] Error ${error}`);
throw error;
}
};

export const getAllDocuments = async ({
collectionName,
sortBy = "",
sortOrder = "asc",
}: {
collectionName: string;
sortBy?: string;
sortOrder?: "asc" | "desc";
}): Promise<{
documents: any[];
count: number;
}> => {
try {
let q;
if (sortBy) {
q = query(collection(db, collectionName), orderBy(sortBy, sortOrder));
} else {
q = query(collection(db, collectionName));
}

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

if (documents.length > 0) {
console.log(
`Database Service: Successfully retrieved ${documents.length} documents from Collection [${collectionName}]`
);
} else {
console.log(
`Database Service: No documents found in Collection [${collectionName}]`
);
}

return {
documents,
count: querySnapshot.size,
};
} catch (error) {
console.error(
`Database Service: Error fetching documents from Collection [${collectionName}]`,
error
);
throw error;
}
};

export const createRefFromString = ({
collectionName,
idString,
}: {
collectionName: string;
idString: string;
}): DocumentReference => {
const docRef = doc(db, collectionName, idString);
return docRef;
};

export default {
addDocument,
deleteDocument,
updateDocument,
getDocumentById,
getDocumentsByFilters,
getAllDocuments,
addDocumentWithId,
createRefFromString,
};

Conclusion

In conclusion, this JavaScript utility represents a significant leap towards simplifying Firestore interactions in React applications. By abstracting the intricacies of Firestore operations, it allows developers to focus on building features that matter, rather than getting bogged down by database communication code. Whether you’re a seasoned developer or just starting out, integrating this utility into your projects can enhance your development workflow, making your code cleaner, more readable, and more efficient.

--

--

Iqbal Shehzada

AI Engineer, Web Developer and Flutter Developer with experience of 7 years