How To Build Your Own Likes and Comments System With Firebase and React

One of my todo items with React apps was having performance-first dynamic comments and likes system for static websites. Why? Because it brings the capabilities of going beyond content and add functionalities which invite user engagement.

Both Cusdis and Disqus are not performance-friendly as they highly affect Cumulative Layout Shift (CLS).

So I set out on creating the system with Firebase, TailwindCSS and React. While TailwindCSS is not a compulsion, it's my go to library. Let's get started.

Setting Up Firebase

  • Install Firebase (Client Side) with the following command:
npm i firebase
  • Create firebase.js with the following configuration:
// File: @/lib/firebase.js

import 'firebase/firestore'
import firebase from 'firebase/app'

// More about firebase config on https://firebase.google.com/docs/web/setup#config-object
var firebaseConfig = {
  apiKey: '',
  authDomain: '',
  projectId: '',
  storageBucket: '',
  messagingSenderId: '',
  appId: '',
}

if (!firebase.apps.length) {
  firebase.initializeApp(firebaseConfig)
} else {
  firebase.app()
}

export const firestore = firebase.firestore()
export default firebase

Creating Like Component

  • Create the like.js file:
// File: components/blog/like.js

import { firestore } from '@/lib/firebase'
  • Add the getLikes function which takes in the slug of the blog page, and a callback function if needed.
export const getLikes = (slug, callBackFunction) => {
  firestore
    .collection('likes')
    .doc(slug)
    .get()
    .then((doc) => {
      if (doc.exists) {
        callBackFunction(Object.keys(doc.data()).length)
      }
    })
    .catch((err) => {
      console.error(err)
    })
}
  • Add the postLike function which takes in the slug of the blog page, and a callback function if needed.
export const postLike = (slug, callBackFunction) => {
  fetch('https://api.ipify.org/?format=json', {
    method: 'GET',
  })
    .then((res) => res.json())
    .then((res) => {
      firestore
        .collection('likes')
        .doc(slug)
        .set(
          {
            [res['ip']]: null,
          },
          { merge: true }
        )
        .then(callBackFunction)
    })
    .catch((err) => {
      console.error(err)
    })
}

Creating Comment Component

  • Create the comment.js file:
// File: components/blog/comments.js

import { useState } from 'react'
import firebase, { firestore } from '@/lib/firebase'
  • Adding the getComments function which takes in the slug of the blog page, and a callback function if needed.
export const getComments = (slug, callBackFunction) => {
  firestore
    .collection('comments')
    .get()
    .then((snapshot) => {
      const posts = snapshot.docs
        .map((doc) => doc.data())
        .filter((doc) => doc.slug === slug)
        .map((doc) => {
          return { id: doc.id, ...doc }
        })
      callBackFunction(posts)
    })
    .catch((err) => {
      console.log(err)
    })
}
  • Adding the writeComment function which takes in the slug of the blog page, and a callback function if needed.
export const writeComment = (name, slug, content, email, callBackFunction) => {
  let temp = {
    name,
    slug,
    content,
    time: firebase.firestore.Timestamp.fromDate(new Date()),
  }
  if (email.length > 0) temp['email'] = email
  firestore
    .collection('comments')
    .add(temp)
    .then(() => {
      callBackFunction()
    })
    .catch((err) => {
      console.error(err)
    })
}
  • Creating the LoadComments function which takes in the set of the comments to display
export const LoadComments = ({ comments }) => {
  return comments
    .sort((a, b) =>
      a.time.toDate().getTime() > b.time.toDate().getTime() ? -1 : 1
    )
    .map((item) => (
      <div
        key={item.time.seconds}
        className="border dark:border-gray-500 rounded p-5 w-full mt-5 flex flex-col"
      >
        <span className="text-lg text-gray-500 dark:text-gray-300 font-medium">
          {item.name} &middot; {item.time.toDate().toDateString()}
        </span>
        <span className="mt-3 text-md text-gray-500 dark:text-gray-300">
          {item.content}
        </span>
      </div>
    ))
}
  • Creating the WriteComment component which takes in the slug of the blog page, and setComments for setting the new set of comments to be displayed.
const WriteComment = ({ slug, setComments }) => {
  const [name, setName] = useState('')
  const [email, setEmail] = useState('')
  const [comment, setComment] = useState('')

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        writeComment(name, slug, comment, email, () =>
          getComments(slug, setComments)
        )
        setName('')
        setEmail('')
        setComment('')
      }}
      className="mt-10 flex flex-col w-full"
    >
      <h1 className="font-semibold text-lg">Write a comment</h1>
      <div className="flex flex-col sm:flex-row sm:space-x-5 items-start">
        <input
          required
          value={name}
          placeholder="Name*"
          onChange={(e) => setName(e.target.value)}
          className="mt-5 w-full sm:w-1/2 appearance-none outline-none ring-0 px-5 py-2 border dark:hover:border-white hover:border-black rounded hover:shadow text-black dark:bg-black dark:text-gray-300 dark:border-gray-500"
        />
        <div className="mt-5 w-full sm:w-1/2 flex flex-col space-y-1">
          <input
            value={email}
            placeholder="Email (Optional)"
            onChange={(e) => setEmail(e.target.value)}
            className="w-full appearance-none outline-none ring-0 px-5 py-2 border dark:hover:border-white hover:border-black rounded hover:shadow text-black dark:bg-black dark:text-gray-300 dark:border-gray-500"
          />
          <span className="text-sm text-gray-400">
            Email will remain confidential.
          </span>
        </div>
      </div>
      <textarea
        required
        value={comment}
        onChange={(e) => setComment(e.target.value)}
        placeholder={'Comment*\nMaximum of 500 characters.'}
        className="mt-5 appearance-none outline-none ring-0 pt-5 px-5 pb-10 border dark:hover:border-white hover:border-black rounded hover:shadow text-black dark:bg-black dark:text-gray-300 dark:border-gray-500"
      />
      <button
        type="submit"
        className="w-[200px] appearance-none mt-5 py-2 px-5 text-center rounded border hover:bg-gray-100 dark:hover:bg-[#28282B] dark:border-gray-500"
      >
        Post a comment
      </button>
    </form>
  )
}

export default WriteComment

Creating Dynamic Blog Component

  • Load the components in the dynamic blog [slug].js file:
import WriteComment, {
  getComments,
  LoadComments,
} from '@/components/blog/comments'

export default function Post({ post }) {
  const [comments, setComments] = useState([])
  return <>
    <WriteComment setComments={setComments} slug={post.slug} />
    <div className="mt-10 pt-10 w-full border-t dark:border-gray-500">
      <button
        onClick={() => getComments(post.slug, setComments)}
        className="w-[200px] appearance-none py-2 px-5 text-center rounded border hover:bg-gray-100 dark:hover:bg-[#28282B]   dark:border-gray-500"
      >
        Load Comments
      </button>
    </div>
    <LoadComments comments={comments} />
  </>
}

Example

You can see an example on my blog page! The source code is available here.

23