Skip to content
Chandan Kumar

React Tutorial: Adding comments to Gatsbyjs blog firebase authentication

react, gatsby, firebase3 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

Quick recap

  • Use Firebase authentication to authenticate and authorize the users to comment on the blog
  • Use Firestore to persist blog comments
  • Use Firebase Emulator to have end to end development setup locally
    • so that we don't have to invoke production firebase.
    • This makes development a tick faster as it cuts down external network requests.
    • Not worried about messing up the production data.
    • Firebase emulator makes it easier to develop and test out the implementation.

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

firebase-helper.ts
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 Rules
16export 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 })

firebase-helper.ts
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].displayName
20 : "UNKNOWN";
21 value.authorPhoto = userMap[value.authorId]
22 ? userMap[value.authorId].photoURL
23 : 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}

Local Emulator Setup

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

Earlier post

  1. React Firestore(Firebase) integration
  2. React Firebase Authentication integration
  3. Gatsby blog comments as an end product.

Getting started

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.

  1. Comments: List of comments
  2. Login/Authentication (and logout)
  3. Add comment

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);

Listing comments

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

Configure Firestore rule

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} />

Firebase Authentication

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

comment-section.tsx
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

  • to handle errors and exceptions
  • cleaning up the UI.
login.tsx
1const doFirebaseLogin = (event: React.FormEvent) => {
2 event.preventDefault();
3 signInWithEmailAndPassword(auth, email, password);
4};

Add Comment

In the parent component add a function that writes to Firestore and pass on the function to AddComment component.

comment-section.tsx
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

comment-section.tsx
1<AddComment user={user} addComment={addComment} />

Comments

Copyleft. WTH
Theme by LekoArts