— react, gatsby, firebase — 3 min read
Tutorial on integrating firebase on client side, particularly Firestore. I built a comment system for Gatsby Blog powered by Firebase/Firestore. I am using for Gatsby, although it can be tweaked for any other React application like nextjs or Remix.
Walkthrough video of final implementation
Firebase Comment is integrated with this blog and if you would like to see it in action, before reading through, check out the comments
Down below you can find previous video on hands-on coding.
Source Code: https://github.com/ch4nd4n/chandankumar.com/tree/master/src/components/comment
Grunt of the work to interact with Firebase is done in firebase-helper.ts
It has a method to add comment, fetch comments, login user using google
authentication, delete a comment and update user profile.
ACL is taken care by Firestore rules. A logged in user can comment and view everyone's comment but can't edit other person's comment or delete it. A quick example is something like
allow delete: if resource.data.authorId == request.auth.uid;
Logic to add comment is quite straight forward
1export function addComment(slug, user, comment) {2 const newComment = doc(collection(db, "blogComments"));3 return setDoc(4 newComment,5 {6 slug,7 comment,8 authorId: user.uid,9 timestamp: serverTimestamp(),10 },11 { merge: false }12 );13}14
15// ACL is taken care by Firebase Rules16export function deleteComment(commentId) {17 return deleteDoc(doc(db, "blogComments", commentId));18}
To get comments(getComments
) from firestore, we query the collection for specific
slug and then we populate author detail. I have kept the model very simple
and haven't nested it.
Logic to update logged in user profile(updateProfile
) is straight forward as well.
Use setDoc
to update userProfiles
for given userid, uid
is what firebase
authentication gives.
setDoc(doc(db, "userProfiles", uid), { uid, photoURL, displayName })
1export async function getComments(slug) {2 const commentsCol = query(3 collection(db, "blogComments"),4 where("slug", "==", slug),5 orderBy("timestamp")6 );7 const commentsSnap = await getDocs(commentsCol);8 if (commentsSnap.empty) {9 return [];10 }11 const commentList = commentsSnap.docs.map((doc) => {12 return { ...doc.data(), id: doc.id };13 }) as CommentType[];14 if (!commentList.length) return [];15 const profileIds = commentList.map((comment) => comment.authorId);16 const userMap = await getUserProfiles(profileIds);17 commentList.forEach((value) => {18 value.authorName = userMap[value.authorId]19 ? userMap[value.authorId].displayName20 : "UNKNOWN";21 value.authorPhoto = userMap[value.authorId]22 ? userMap[value.authorId].photoURL23 : null;24 });25
26 return commentList;27}28
29async function getUserProfiles(ids: string[]) {30 const userProfiles = collection(db, "userProfiles");31 const q = query(userProfiles, where("uid", "in", ids));32 const reducer = (prev, cur) => {33 prev[cur.uid] = cur;34 return prev;35 };36 const userList = (await getDocs(q)).docs.map((user) => user.data());37 return userList.reduce(reducer, {});38}39
40export async function updateProfile(user, displayName) {41 const { uid, photoURL } = user;42 const docSnap = await getDoc(doc(db, "userProfiles", uid));43 if (docSnap.exists()) {44 await setDoc(doc(db, "userProfiles", uid), { uid, photoURL, displayName });45 }46}
Refer to firestore.rules
that I am using for local development
1service cloud.firestore {2 match /databases/{database}/documents {3 match /{document=**} {4 allow read, write: if false;5 }6 match /blogComments/{multiSegment=**} {7 allow read;8 allow create: if request.auth != null;9 allow delete: if resource.data.authorId == request.auth.uid;10 }11 match /userProfiles/{multiSegment=**} {12 allow read;13 allow create: if request.auth != null;14 allow update: if request.auth.uid == resource.data.uid;15 }16 }17}
Setup Emulator: Firebase emulator makes development easy. It's one time effort setting it up.
1npm install -g firebase-tools
Choose a folder where you would want to initialize firebase emulator. This process would create emulator related files, so be wary of where you init the emulator. When you initialize the emulator, do remember to enable firestore and authentication.
1firebase init emulators
start the emulator
If you don't care about persisting emualtor data between restart
1firebase emulators:start
or if you want to persist the data use the below
1firebase emulators:start --import=./.emulator-data --export-on-exit
There are solutions like Disqus, which I have written about in the past, but I did not realize at the time that it comes with its issues of targetted ads, user tracking et al.
I am wrapping comments functionality in a React component
CommentSection
which has 3 sub-components. Oh, and before I forget
I highly recommend using Firebase Emulator instead of using
production Firebase.
A grunt of the logic is in CommentSection
, and the rest of the
components are more or less pure components.
So we begin with getting a reference to Firestore and initialize firebase with the necessary configuration.
1db = getFirestore(app);2app = initializeApp(firebaseConfig);
The next step is to write a function that fetches comments, so
if comments are in a collection called blogPosts
(I know, not
the best collection name to store comments), We query it like
1const commentsCol = query(2 collection(db, "blogPosts"),3 where("slug", "==", slug)4);
Iterate over this Firestore collection and convert it list of comments like below.
Remember to update Firestore rule to allow read and write
Firestore rules are required to prevent unauthorized access or abuse
1match /blogPosts/{multiSegment=**} {2 allow read;3 allow write: if request.auth != null;4}
Why firestore rules?
With Cloud Firestore Security Rules, you can focus on building a great user experience without having to manage infrastructure or write server-side authentication and authorization code. https://firebase.google.com/docs/firestore/security/get-started
1const commentSnapshot = await getDocs(commentsCol);2const commentList = commentSnapshot.docs.map((doc) => {3 return { ...doc.data(), id: doc.id };4});
We add a component to render comments which is
1const Comment = (prop) => {2 const { getComments, comments } = prop;3
4 useEffect(() => {5 getComments();6 }, []);7
8 return (9 <div>10 <h3>Comments</h3>11 <ul>12 {comments.map((comment) => (13 <li key={comment.id}>{comment.comment}</li>14 ))}15 </ul>16 </div>17 );18};
and the parent component will pass the comments reference
1<Comment getComments={getComments} comments={comments} />
For authentication, I am using a rudimentary form that lets a user enter username and password
We get firebase auth reference in the parent component and pass it to the child component
1const auth = getAuth();
When the user clicks the "login" button we use signInWithEmailAndPassword
from firebase
to authenticate the user. There are a lot of things to be cleaned up like
1const doFirebaseLogin = (event: React.FormEvent) => {2 event.preventDefault();3 signInWithEmailAndPassword(auth, email, password);4};
In the parent component add a function that writes to Firestore and
pass on the function to AddComment
component.
1async function addComment(comment) {2 const newComment = doc(collection(db, "/blogPosts"));3 setDoc(newComment, { slug, comment }, { merge: false });4}
and pass on the reference to AddComment
1<AddComment user={user} addComment={addComment} />