DEV Community

loading...
Cover image for How To Build Your Own Likes and Comments System With Firebase and React

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

Rishi Raj Jain
Hey, I'm Rishi Raj Jain. I build products that delight customers through a blend of frontend engineering and visual design.
Originally published at rishi.app ・4 min read

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
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

Creating Like Component

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

import { firestore } from '@/lib/firebase'
Enter fullscreen mode Exit fullscreen mode
  • 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)
    })
}
Enter fullscreen mode Exit fullscreen mode
  • 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)
    })
}
Enter fullscreen mode Exit fullscreen mode

Creating Comment Component

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

import { useState } from 'react'
import firebase, { firestore } from '@/lib/firebase'
Enter fullscreen mode Exit fullscreen mode
  • 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)
    })
}
Enter fullscreen mode Exit fullscreen mode
  • 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)
    })
}
Enter fullscreen mode Exit fullscreen mode
  • 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>
    ))
}
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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} />
  </>
}
Enter fullscreen mode Exit fullscreen mode

Example

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

Discussion (2)

Collapse
karanpratapsingh profile image
Karan Pratap Singh

Well done!

Collapse
digitalplayer1125 profile image
Rishi Raj Jain Author

Thank you! @karanpratapsingh